Project import generated by Copybara.

GitOrigin-RevId: 2eacec50a75dba903a1f592d753e2fbbf61ec06e
diff --git a/.pre-commit-config.beta.yaml b/.pre-commit-config.beta.yaml
new file mode 100644
index 0000000..0b9d17f
--- /dev/null
+++ b/.pre-commit-config.beta.yaml
@@ -0,0 +1,71 @@
+# See go/cast-pre-commit for details
+default_language_version:
+    python: python3
+repos:
+-   hooks:
+    -   args: [--root, ../chromium/src, --target, chromecast/internal, --extra-repos, chromecast/internal]
+
+        id: check-deps
+        name: chromium-src-check-deps
+    -   args: [--root, ../chromium/src, --target, libinteraction, --extra-repos, libinteraction,
+    --extra-repos, libinteraction/shared]
+
+        id: check-deps
+        name: libinteraction-check-deps
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/check-deps
+    rev: '1.2'
+-   hooks:
+    -   args: [--chromium-buildtools-path, ../chromium/src/buildtools]
+
+        id: clang-format
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/clang-format
+    rev: '2.4'
+-   hooks:
+    -   args: [--diff-only]
+
+        id: cpp-blacklist
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/cpp-blacklist
+    rev: '1.2'
+-   hooks:
+    -   args: []
+
+        id: cpplint
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/cpplint
+    rev: '1.2'
+-   hooks:
+    -   id: eureka-commit-message-check
+        stages:
+        - commit-msg
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/eureka-commit-message-check
+    rev: '1.4'
+-   hooks:
+    -   id: eureka-prepare-commit-message
+        stages:
+        - prepare-commit-msg
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/eureka-prepare-commit-message
+    rev: '1.4'
+-   hooks:
+    -   id: json-format-check
+    -   id: xml-format-check
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/file-format-check
+    rev: '0.2'
+-   hooks:
+    -   args: [../chromium/src]
+
+        id: gn-check
+        name: gn-check-default
+    -   args: [--args, is_chromecast=true chromecast_branding="internal" is_cast_audio_only=true,
+    ../chromium/src]
+
+        id: gn-check
+        name: gn-check-audioonly
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/gn-check
+    rev: '0.2'
+-   hooks:
+    -   id: gn-format
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/gn-format
+    rev: '1.6'
+-   hooks:
+    -   id: pylint
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/pylint
+    rev: '2.0'
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..d4b5249
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,78 @@
+# See go/cast-pre-commit for details
+default_language_version:
+    python: python3
+repos:
+-   hooks:
+    -   args: [--root, ../chromium/src, --target, chromecast/internal, --extra-repos, chromecast/internal]
+
+        id: check-deps
+        name: chromium-src-check-deps
+        stages:
+        - push
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/check-deps
+    rev: '1.3'
+-   hooks:
+    -   args: []
+
+        id: clang-format
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/clang-format
+    rev: '3.4'
+-   hooks:
+    -   args: []
+
+        id: cpp-blacklist
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/cpp-blacklist
+    rev: '2.4'
+-   hooks:
+    -   args: []
+
+        id: cpplint
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/cpplint
+    rev: '1.4'
+-   hooks:
+    -   id: eureka-commit-message-check
+        stages:
+        - commit-msg
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/eureka-commit-message-check
+    rev: '1.9'
+-   hooks:
+    -   id: eureka-prepare-commit-message
+        stages:
+        - prepare-commit-msg
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/eureka-prepare-commit-message
+    rev: '1.5'
+-   hooks:
+    -   id: json-format-check
+    -   id: xml-format-check
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/file-format-check
+    rev: '0.4'
+-   hooks:
+    -   args: [--args, import("//chromecast/internal/build/args/config/precommit/all.gni"), ../chromium/src]
+
+        id: gn-check
+        name: gn-check-default
+        stages:
+        - push
+    -   args: [--args, import("//chromecast/internal/build/args/config/precommit/audio-only.gni"),
+    ../chromium/src]
+
+        id: gn-check
+        name: gn-check-audioonly
+        stages:
+        - push
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/gn-check
+    rev: '2.2'
+-   hooks:
+    -   id: gn-format
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/gn-format
+    rev: '2.0'
+-   hooks:
+    -   id: pylint
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/pylint
+    rev: '2.2'
+-   hooks:
+    -   id: precommit-message-preservation
+        stages:
+        - prepare-commit-msg
+    repo: sso://nest-source-internal.git.corp.google.com/hooks/precommit-message-preservation
+    rev: '1.3'
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/__init__.py
diff --git a/build/__init__.py b/build/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/build/__init__.py
diff --git a/build/get_latest_build.py b/build/get_latest_build.py
new file mode 100755
index 0000000..a38d2cd
--- /dev/null
+++ b/build/get_latest_build.py
@@ -0,0 +1,905 @@
+"""Script to identify the latest build number for testing.
+
+This script is called before each test to identify the latest build number and
+path to the latest build zip file.
+
+By default, we find the common set of two build flavors (eng and user) and then
+search for a good build hint from the greatest build number, once such hint is
+found, the build number will be returned.
+
+Two types of build hints are currently in-place:
+1. (post-unit-test) good_to_test hint is created after unit test completion
+(regardless test pass or fail). It is used by regular test.
+2. (pre-unit-test) test_deps.zip hint is created when OTA and test binaries are
+successfully produced. It is used by unit test.
+
+Typical usages:
+1. Get good build for regular test:
+  get_latest_build.py --output_mode="number" --branch=1.8 --product=anchovy
+2. Get a good build for unit test:
+  get_latest_build.py --output_mode="number" --branch=1.8 --product=anchovy
+--flavor=eng --build_triangulation_list=eng --use_unit_test_hint
+3. Get a good build for unit test and claim it (no one will get this build)
+  get_latest_build.py --output_mode="number" --branch=1.8 --product=anchovy
+--flavor=eng --build_triangulation_list=eng --use_unit_test_hint
+--claim_unit_test_hint
+
+One the build is clained, the hint file name will change and will not be
+discovered by another build searcher.
+"""
+
+import datetime
+import optparse
+import os
+import random
+import re
+import subprocess
+import sys
+import tempfile
+import time
+
+__author__ = 'billiani@google.com (Leonardo Billiani)'
+
+# Commands to list the build zip files:
+# Eg. path: gs://gtv-te/internal/gtv-3.0/cosmo-tests/32908/cosmo-tests-32908.zip
+GS_LIST_BUILDS_CMD = 'gsutil ls gs://%s/%s/%s-%s/%s/%s'
+# gs://gtv-eureka/internal/1.28f/biggie-eng/100872/factory/biggie-fct-100872.zip
+GS_LIST_FCT_BUILDS_CMD = 'gsutil ls gs://%s/%s/%s-%s/%s/factory/%s'
+OTA_FILE_PATTERN = '%s-%s-%s.zip'
+OTA_FILE_PATTERN_WITH_BOARD = '%s-%s-%s-%s.zip'
+GS_COPY_BUILD_CMD = 'gsutil cp %s %s'
+
+# Path to marker file
+GS_LIST_FILE = 'gsutil ls gs://%s/%s/%s-%s/%s/%s'
+
+# Rename marker file
+GS_MV_MARKER_CMD = 'gsutil mv %s %s'
+
+# Regex to extract build number from a build zip path:
+# Eg. 32 from gs://gtv-te/internal/gtv-3.0/cosmo-tests/32/cosmo-tests-32.zip
+BUILD_NUMBER_RE = re.compile(r'\/(\d+)\/')
+
+# Supported "flavors" and "output type" of build:
+BUILD_FLAVORS = {
+    'darcy': 'arm',
+    'eng': 'eng',
+    'eng-clang': 'eng-clang',
+    'fugu': 'x86',
+    'imx7d_pico': 'arm',
+    'molly': 'arm',
+    'tests': 'tests',
+    'user': 'user'}
+
+BUILD_OUTPUT_TYPES = {
+    'fct': 'fct',
+    'ota': 'ota',
+    'symbols': 'symbols',
+    'tests': 'tests'}
+
+# Chromecast Product Cloud Storage Bucket
+PRODUCT_GCS_BUCKET = 'gtv-eureka'
+
+# Chromecast Unit Test Binary Cloud Storage Bucket
+UNIT_TEST_GCS_BUCKET = 'chromecast-unit-test'
+
+# Root repositories for different products
+PRODUCT_ROOT_REPOSITORY = {
+    'anchovy': '%s/internal' % PRODUCT_GCS_BUCKET,
+    'androidtv': '%s/internal' % PRODUCT_GCS_BUCKET,
+    'androidthings': '%s/internal' % PRODUCT_GCS_BUCKET,
+    'assistantdefault': '%s/internal' % PRODUCT_GCS_BUCKET,
+    'audiodefault': '%s/cast_audio_reference' % PRODUCT_GCS_BUCKET,
+    'biggie': '%s/internal' % PRODUCT_GCS_BUCKET,
+    'bubbletea': '%s/cast_tv_mstar' % PRODUCT_GCS_BUCKET,
+    'chorizo': '%s/internal' % PRODUCT_GCS_BUCKET,
+    'dragonfly': '%s/cast_tv' % PRODUCT_GCS_BUCKET,
+    'estelle': '%s/internal' % PRODUCT_GCS_BUCKET,
+    'firefly': '%s/cast_tv' % PRODUCT_GCS_BUCKET,
+    'guabao': '%s/cast_tv_mstar' % PRODUCT_GCS_BUCKET,
+    'Mars_US': '%s/cast_tv_mediatek' % PRODUCT_GCS_BUCKET,
+    'mozart': '%s/cast_audio_new' % PRODUCT_GCS_BUCKET,
+    'mushroom': '%s/internal' % PRODUCT_GCS_BUCKET,
+    'N00007': '%s/cast_tv_novatek' % PRODUCT_GCS_BUCKET,
+    'np8340': '%s/cast_audio_new' % PRODUCT_GCS_BUCKET,
+    'pepperoni': '%s/internal' % PRODUCT_GCS_BUCKET,
+    'pineapple': '%s/internal' % PRODUCT_GCS_BUCKET,
+    'redwood': '%s/cast_tv_marvell' % PRODUCT_GCS_BUCKET,
+    'salami': '%s/internal' % PRODUCT_GCS_BUCKET,
+    'steak': '%s/internal' % PRODUCT_GCS_BUCKET,
+    'sullivan': '%s/cast_audio_new' % PRODUCT_GCS_BUCKET,
+    'suncake': '%s/cast_tv_mstar' % PRODUCT_GCS_BUCKET,
+    'taroball': '%s/cast_tv_mstar' % PRODUCT_GCS_BUCKET,
+    'wutang': '%s/cast_audio_new' % PRODUCT_GCS_BUCKET}
+UNIT_TEST_ROOT_REPOSITORY = {
+    'anchovy': '%s/internal' % UNIT_TEST_GCS_BUCKET,
+    'assistantdefault': '%s/internal' % UNIT_TEST_GCS_BUCKET,
+    'audiodefault': '%s/cast_audio_reference' % UNIT_TEST_GCS_BUCKET,
+    'biggie': '%s/internal' % UNIT_TEST_GCS_BUCKET,
+    'bubbletea': '%s/cast_tv_mstar' % UNIT_TEST_GCS_BUCKET,
+    'chorizo': '%s/internal' % UNIT_TEST_GCS_BUCKET,
+    'default': '%s/internal' % UNIT_TEST_GCS_BUCKET,
+    'dragonfly': '%s/cast_tv' % UNIT_TEST_GCS_BUCKET,
+    'estelle': '%s/internal' % UNIT_TEST_GCS_BUCKET,
+    'firefly': '%s/cast_tv' % UNIT_TEST_GCS_BUCKET,
+    'guabao': '%s/cast_tv_mstar' % UNIT_TEST_GCS_BUCKET,
+    'Mars_US': '%s/cast_tv_mediatek' % UNIT_TEST_GCS_BUCKET,
+    'mozart': '%s/cast_audio_new' % UNIT_TEST_GCS_BUCKET,
+    'mushroom': '%s/internal' % UNIT_TEST_GCS_BUCKET,
+    'N00007': '%s/cast_tv_novatek' % UNIT_TEST_GCS_BUCKET,
+    'np8340': '%s/cast_audio_new' % UNIT_TEST_GCS_BUCKET,
+    'pepperoni': '%s/internal' % UNIT_TEST_GCS_BUCKET,
+    'pineapple': '%s/internal' % UNIT_TEST_GCS_BUCKET,
+    'redwood': '%s/cast_tv2' % UNIT_TEST_GCS_BUCKET,
+    'salami': '%s/internal' % UNIT_TEST_GCS_BUCKET,
+    'steak': '%s/internal' % UNIT_TEST_GCS_BUCKET,
+    'sullivan': '%s/cast_audio_new' % UNIT_TEST_GCS_BUCKET,
+    'suncake': '%s/cast_tv_mstar' % UNIT_TEST_GCS_BUCKET,
+    'taroball': '%s/cast_tv_mstar' % UNIT_TEST_GCS_BUCKET,
+    'wutang': '%s/cast_audio_new' % UNIT_TEST_GCS_BUCKET}
+
+STD_ROOT_REPOSITORY = 'gtv-te'
+
+# Run modes for this script:
+OUTPUT_MODES = {'path': 'path', 'number': 'number'}
+
+# Supported tablet models:
+TABLETS = ['xoomtablet', 'nexus10tablet']
+
+# Supported Android products:
+ANDROID_PRODUCTS = ['androidtv', 'androidthings', 'soba']
+
+# Build flavor list is used to triangulate a good build. For example, by
+# default, we get the greatest common build number from both eng and user
+# build.
+DEFAULT_TRIANGULATION_LIST = (
+    ' '.join([BUILD_FLAVORS['eng'], BUILD_FLAVORS['user']]))
+
+# List of repositories that only build eng flavor
+ENG_ONLY_REPOSITORY_LIST = ['chromium_tot']
+
+# A good build marker file is generated when a build can finish unit test.
+GOOD_BUILD_MARKER = 'good_to_test'
+
+# Directory with all the test identifiers
+TEST_IDENTIFIER_DIR = 'run_identifiers'
+
+# Run identifiers path
+IDS_REPOSITORY = 'fragglerock/%s' % TEST_IDENTIFIER_DIR
+
+# Unit test marker is the zip file which is used for unit test slaves.
+UNIT_TEST_MARKER_DIR = 'gs://%s/%s/%s-%s/%s/'
+UNIT_TEST_MARKER = 'test_deps.tar.gz'
+CLAIMED_UNIT_TEST_MARKER = 'test_deps_claimed.tar.gz'
+
+BAD_BUILD_NUMBER = '0'
+RANDOM_BACKOFF_SECS = 60
+DEFAULT_MAX_BUILD_LIST_LEN = 50
+
+# Cast OTA file name override
+# Key=hardware-type, Value=OTA-file-name
+CAST_OTA_OVERRIDE_DICT = {
+    'bubbletea': 'usb.bin',
+    'guabao': 'usb.bin',
+    'Mars_US': 'upgrade_loader_no_perm_data.pkg',
+    'mozart': 'mozart-b2-ota.zip',
+    'N00007': 'update.img',
+    'np8340': '8506_linux_demo_dbg.bin',
+    'sullivan': '8580_linux_demo_dbg.bin',
+    'suncake': 'usb.bin',
+    'taroball': 'usb.bin',
+    'wutang': '8507_linux_demo_dbg.bin'}
+
+# Assumption: all products should have eng builds
+# Overrides the triangulation list for products without user builds.
+TRIANGULATION_OVERRIDE_DICT = {
+    'bubbletea': 'eng',
+    'dragonfly': 'eng',
+    'guabao': 'eng',
+    'Mars_US': 'eng',
+    'mozart': 'eng',
+    'N00007': 'eng',
+    'np8340': 'eng',
+    'sullivan': 'eng',
+    'suncake': 'eng',
+    'taroball': 'eng',
+    'wutang': 'eng'}
+
+# Options for the build selection policy.
+ALWAYS_USE_LATEST = 'always_use_latest'
+BACKTRACK_THEN_LATEST = 'backtrack_then_latest'
+ONLY_BACKTRACK = 'only_backtrack'
+SECOND_TO_LATEST = 'second_to_latest'
+BUILD_SELECTION_POLICIES = [
+    ALWAYS_USE_LATEST,
+    BACKTRACK_THEN_LATEST,
+    ONLY_BACKTRACK,
+    SECOND_TO_LATEST
+]
+DEFAULT_BUILD_SELECTION_POLICY = ONLY_BACKTRACK
+
+
+def ParseFlags():
+  """Parses command line flags."""
+  parser = optparse.OptionParser()
+  parser.add_option('--branch',
+                    default='master',
+                    help='branch to get latest build number')
+  parser.add_option('--product',
+                    help='product to get latest build number')
+  parser.add_option('--build_output_type',
+                    default=BUILD_OUTPUT_TYPES['ota'],
+                    type='choice',
+                    choices=list(BUILD_OUTPUT_TYPES.keys()),
+                    help=str(list(BUILD_OUTPUT_TYPES.keys())))
+  parser.add_option('--build_number',
+                    help='build number')
+  parser.add_option('--build_output_path',
+                    default='',
+                    help=('Complete path to where the build file is copied'))
+  parser.add_option('--output_mode',
+                    type='choice',
+                    choices=list(OUTPUT_MODES.keys()),
+                    help=str(list(OUTPUT_MODES.keys())))
+  parser.add_option('--flavor',
+                    default=BUILD_FLAVORS['eng'],
+                    type='choice',
+                    choices=list(BUILD_FLAVORS.keys()),
+                    help=str(list(BUILD_FLAVORS.keys())))
+  parser.add_option('--build_triangulation_list',
+                    default=DEFAULT_TRIANGULATION_LIST,
+                    help='list of flavors used to determine latest build')
+  parser.add_option('--use_unit_test_hint',
+                    action='store_true',
+                    default=False,
+                    help='Use unit test zip file as marker for a good build')
+  parser.add_option('--claim_unit_test_hint',
+                    action='store_true',
+                    default=False,
+                    help='Change the unit test marker file name if True')
+  parser.add_option('--board_type',
+                    default='none',
+                    help='Chromecast device board type, e.g. lexx-b3')
+  parser.add_option('--repo_alias',
+                    default='',
+                    help='Repository alias, e.g. internal or partner')
+  parser.add_option('--test_identifier',
+                    default='',
+                    help=('Test identifier from buildbot. If specified, '
+                          'it gets the latest non tested build.'))
+  parser.add_option('--use_gn',
+                    default=1,
+                    type=int,
+                    help=('Positive integers for using GN; otherwise, no GN.'))
+  parser.add_option('--build_selection_policy',
+                    type='choice',
+                    choices=[ALWAYS_USE_LATEST,
+                             BACKTRACK_THEN_LATEST,
+                             ONLY_BACKTRACK,
+                             SECOND_TO_LATEST],
+                    default=ONLY_BACKTRACK,
+                    help=('Defines the policy to select a build depending '
+                          'on if it\'s been tested or not. '
+                          '"%s" chooses the latest build, whether it\'s '
+                          'been tested or not. '
+                          '"%s" chooses that latest non-tested build '
+                          'or the latest available if all have been tested. '
+                          '"%s" chooses that latest non-tested build '
+                          'and exits with failure if all have been tested. '
+                          '"%s" chooses the second to latest good build. '
+                          'Default: %%default'
+                          % (ALWAYS_USE_LATEST,
+                             BACKTRACK_THEN_LATEST,
+                             ONLY_BACKTRACK,
+                             SECOND_TO_LATEST)))
+  parser.add_option('--max_build_selection_range',
+                    default=DEFAULT_MAX_BUILD_LIST_LEN,
+                    type=int,
+                    help=('Maximum number of builds to be used in build '
+                          'selection process. This flag is used in conjunction '
+                          'with build_selection_policy'))
+  parser.add_option('-v',
+                    action='store_true',
+                    default=False,
+                    help='Run in unit test mode')
+  (flags, _) = parser.parse_args()
+  if flags.v:
+    return None
+  if not flags.output_mode:
+    parser.print_help()
+    sys.exit()
+  if flags.build_number and flags.output_mode == 'build':
+    parser.print_help()
+    sys.exit()
+  return flags
+
+
+test_exit = subprocess.call('gsutil ls &> /dev/null', shell=True)
+if test_exit:
+  print('gsutil not available')
+  sys.exit()
+
+
+class AndroidTvBuildHelper(object):
+  """Class to handle Android TV builds."""
+  # Cloud Storage location (this is pre-defined)
+  GS_PREFIX = 'gs://%s' % UNIT_TEST_ROOT_REPOSITORY['default']
+  # Product Cast Shell APK location
+  GS_APK_PREFIX = 'gs://%s' % PRODUCT_ROOT_REPOSITORY['androidtv']
+  # Cast Shell APK file pattern
+  CAST_SHELL_APK = 'CastShellInternal-*-%s.apk'
+  # List command
+  GS_LIST_CMD = 'gsutil ls'
+
+  # Supported flavors: x86 for Fugu and arm for Molly.
+  SUPPORTED_FLAVOR = ['x86', 'arm']
+  # Supported scope: internal or public
+  SUPPORTED_SCOPE = ['internal', 'public']
+
+  def __init__(self, product='androidtv', use_gn=1):
+    """Constructor."""
+    self._product = product
+    self._gn_dir_suffix = self._GetGnDirSuffix(use_gn)
+
+  def _GetGnDirSuffix(self, use_gn):
+    """Returns gn directory suffix."""
+    if use_gn <= 0:
+      return ''
+    return '_gn'
+
+  def _GetGsDirPattern(self):
+    """Returns GCS directory pattern."""
+    return '%s/%s/content_shell_'+ self._product + '_%s_%s%s/*/%s'
+
+  def GetLatestBuildNumber(self,
+                           branch,
+                           flavor,
+                           claim_hint=True,
+                           test_identifier='',
+                           scope='internal'):
+    """Returns the most recent un-test build number.
+
+    Args:
+      branch: String of branch name, e.g. unfork_m37.
+      flavor: String for device flavor, e.g. fugu or molly.
+      claim_hint: Optional boolean if a hint needs to be claimed.
+      test_identifier: Optional string for test complete hint.
+      scope: Optional string for scope defined, interna or public.
+
+    Returns:
+      String: of the most recent build number.
+    """
+    build_to_return = 0
+    if claim_hint:
+      gs_prefix = self.GS_PREFIX
+      marker = UNIT_TEST_MARKER
+    else:
+      gs_prefix = self.GS_APK_PREFIX
+      marker = GOOD_BUILD_MARKER
+    search_dir = self._GetGsDirPattern() % (gs_prefix,
+                                            branch,
+                                            BUILD_FLAVORS[flavor],
+                                            scope,
+                                            self._gn_dir_suffix,
+                                            marker)
+    gs_list_cmd = ' '.join([self.GS_LIST_CMD, search_dir])
+    output = _Execute(gs_list_cmd)
+    get_build_num_regex = re.compile(search_dir.replace('*', r'(\d+)'))
+    search_dir_list = sorted(
+        output.split(), reverse=True)[:DEFAULT_MAX_BUILD_LIST_LEN]
+    for line in search_dir_list: # pylint: disable=too-many-nested-blocks
+      match = get_build_num_regex.search(line)
+      if match:
+        if match.group(1) > build_to_return:
+          if not test_identifier:
+            build_to_return = match.group(1)
+          else:
+            # Check if the test was completed already
+            if test_identifier and not claim_hint:
+              test_name = test_identifier.split('#')[0]
+              is_tested_cmd = GS_LIST_FILE % (
+                  IDS_REPOSITORY,
+                  branch,
+                  flavor,
+                  'eng',
+                  match.group(1),
+                  os.path.join(TEST_IDENTIFIER_DIR, test_name + '*'))
+              if not _Execute(is_tested_cmd):
+                build_to_return = match.group(1)
+
+    return str(build_to_return)
+
+  def ClaimBuildNumber(self,
+                       build_number,
+                       branch,
+                       flavor='fugu',
+                       scope='internal'):
+    """Renames the test zip file.
+
+    Args:
+      build_number: String containing a build number.
+      branch: String of branch name, e.g. unfork_m37.
+      flavor: String for device flavor, e.g. arm or x86.
+      scope: String for scope defined, interna or public.
+    """
+    path_pattern = self._GetGsDirPattern().replace('*', build_number)
+    original_test_file = (
+        path_pattern % (self.GS_PREFIX,
+                        branch,
+                        BUILD_FLAVORS[flavor],
+                        scope,
+                        self._gn_dir_suffix,
+                        UNIT_TEST_MARKER))
+    claimed_test_file = (
+        path_pattern % (self.GS_PREFIX,
+                        branch,
+                        BUILD_FLAVORS[flavor],
+                        scope,
+                        self._gn_dir_suffix,
+                        CLAIMED_UNIT_TEST_MARKER))
+    _Execute(GS_MV_MARKER_CMD % (original_test_file, claimed_test_file))
+
+  def GetBuildPath(self,
+                   build_number,
+                   branch,
+                   flavor='fugu',
+                   scope='internal',
+                   claim_hint=True):
+    """Renames the test zip file.
+
+    Args:
+      build_number: String containing a build number.
+      branch: String of branch name, e.g. unfork_m37.
+      flavor: Optional string for device flavor, e.g. fugu or molly.
+      scope: Optional string for scope defined, interna or public.
+      claim_hint: Optional boolean if build hint needs to be claimed.
+
+    Returns:
+      String: of path to the claimed test zip file.
+    """
+    if claim_hint:
+      gs_prefix = self.GS_PREFIX
+      marker = CLAIMED_UNIT_TEST_MARKER
+    else:
+      gs_prefix = self.GS_APK_PREFIX
+      marker = self.CAST_SHELL_APK % BUILD_FLAVORS[flavor]
+    path_pattern = self._GetGsDirPattern() % (gs_prefix,
+                                              branch,
+                                              BUILD_FLAVORS[flavor],
+                                              scope,
+                                              self._gn_dir_suffix,
+                                              marker)
+
+    return path_pattern.replace('*', build_number)
+
+
+def _Execute(command, exit_on_stderr=False):
+  """Executes given command and returns stdout string."""
+  proc = subprocess.Popen(
+      command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+  stdoutdata, stderrdata = proc.communicate()
+  if exit_on_stderr:
+    error_message_list = (
+        ['CommandException',
+         'InvalidUriError',
+         'GSResponseError'])
+    for error_message in error_message_list:
+      if error_message in stderrdata:
+        raise IOError('%s: %s' % (command, stderrdata))
+  return stdoutdata
+
+
+def GetRepoGcsRootPath(product, repo_alias=''):
+  """Returns Cloud Storage root path for a given product."""
+  # If repository alias is provided, use it for the GCS root
+  # Otherwise, look it up from the pre-defined product list
+  if repo_alias:
+    root_path = '%s/%s' % (PRODUCT_GCS_BUCKET, repo_alias)
+  else:
+    root_path = PRODUCT_ROOT_REPOSITORY.get(product, STD_ROOT_REPOSITORY)
+
+  return root_path
+
+
+def GetUnitTestGcsRootPath(product, repo_alias=''):
+  """Returns Unit Test Cloud Storage root path for a given product."""
+  # If repository alias is provided, use it for the GCS root
+  # Otherwise, look it up from the pre-defined product list
+  if repo_alias:
+    root_path = '%s/%s' % (UNIT_TEST_GCS_BUCKET, repo_alias)
+  else:
+    root_path = UNIT_TEST_ROOT_REPOSITORY.get(product,
+                  UNIT_TEST_ROOT_REPOSITORY['default'])
+
+  return root_path
+
+
+def GetListBuildsCommand(**kwargs):
+  """Returns command to list builds matching the given parameters.
+
+  Args:
+    **kwargs: Key/Value dictionary containing values for:
+      branch, product, build_number, flavor, build_output_type.
+
+  Returns:
+    String containing command to list the builds.
+  """
+  if not kwargs.get('build_number'):
+    kwargs['build_number'] = '[0-9]*'
+
+  kwargs['root_path'] = GetRepoGcsRootPath(
+      kwargs['product'],
+      kwargs.get('repo_alias'))
+  ota_file_name = ''
+  if kwargs and kwargs.get('board_type') != 'none':
+    ota_file_name = OTA_FILE_PATTERN_WITH_BOARD % (
+        kwargs['product'],
+        kwargs['build_output_type'],
+        kwargs.get('board_type'),
+        kwargs['build_number'])
+  else:
+    ota_file_name = OTA_FILE_PATTERN % (
+        kwargs['product'],
+        kwargs['build_output_type'],
+        kwargs['build_number'])
+  # Override the OTA file name for specific devices (which don't use OTA zip).
+  if kwargs.get('product') in CAST_OTA_OVERRIDE_DICT.keys():
+    ota_file_name = CAST_OTA_OVERRIDE_DICT[kwargs['product']]
+
+  list_cmd_fmt = GS_LIST_BUILDS_CMD
+  # Override the command when OTA output type is fct for factory build.
+  if kwargs and kwargs.get('build_output_type') == 'fct':
+    list_cmd_fmt = GS_LIST_FCT_BUILDS_CMD
+  list_cmd = list_cmd_fmt % (
+      kwargs['root_path'],
+      kwargs['branch'],
+      kwargs['product'],
+      kwargs['flavor'],
+      kwargs['build_number'],
+      ota_file_name)
+
+  return list_cmd
+
+
+def GetBuildsListByFlavor(repo_alias, branch, product, flavor, board_type):
+  """Returns list of eng build paths matching the specified parameters."""
+  return _Execute(
+      GetListBuildsCommand(
+          repo_alias=repo_alias,
+          branch=branch,
+          product=product,
+          flavor=flavor,
+          build_output_type=BUILD_OUTPUT_TYPES['ota'],
+          board_type=board_type)).split()
+
+
+def ToBuildNumber(build_path):
+  """Returns a build number for the build zip at build_path.
+
+  If a build number can not be parsed, 0 is returned.
+
+  Args:
+    build_path: String containing build file path.
+
+  Returns:
+    String containing the build number.
+  """
+  match = BUILD_NUMBER_RE.search(build_path)
+  if match:
+    return match.group(1)
+  return '0'
+
+
+def _IsBuildMarked(command_pattern, build_num, marker_file_name):
+  """Executes the command and returns if the marker is in stdout."""
+  command = command_pattern % (build_num, marker_file_name)
+  stdout = _Execute(command)
+  return marker_file_name in stdout
+
+
+def _IsBuildTested(has_been_tested_cmd, build_num):
+  """Executes the command and returns if the test ID is in stdout."""
+  if not has_been_tested_cmd:
+    return False
+  command = has_been_tested_cmd % (build_num)
+  stdout = _Execute(command)
+  # Only match the first part of the test_identifier
+  test_name = FLAGS.test_identifier.split('#')[0]
+  return test_name in stdout
+
+
+def GetBuildNumberWithMarker(
+    build_number_list,
+    command_pattern,
+    marker_file_name=GOOD_BUILD_MARKER,
+    has_been_tested_cmd='',
+    build_selection_policy=DEFAULT_BUILD_SELECTION_POLICY,
+    max_build_selection_range=DEFAULT_MAX_BUILD_LIST_LEN):
+  """Iterates all the builds and returns the greatest number.
+
+  Examine the build number list from high to low, if a marker file is found,
+  return that build number.
+
+  Args:
+    build_number_list: List containing builds in integers.
+    command_pattern: String containing command with build number to replace.
+    marker_file_name: Optional string containing the marker file name.
+    has_been_tested_cmd: String with the command to verify the test did not run.
+    build_selection_policy: The policy used to select the build.
+    max_build_selection_range: The maximum number of builds to be used for
+        build selection process.
+
+  Returns:
+    String: build number found or zero if not found.
+  """
+  is_good = lambda n: _IsBuildMarked(command_pattern, n, marker_file_name)
+  is_tested = lambda n: _IsBuildTested(has_been_tested_cmd, n)
+  if isinstance(build_number_list, list):
+    build_nums = sorted(
+        build_number_list, reverse=True)[:max_build_selection_range]
+    first_good = None
+    for build_num in build_nums:
+      if not is_good(build_num):
+        continue
+      tested = is_tested(build_num)
+      if first_good:
+        if not tested and build_selection_policy == SECOND_TO_LATEST:
+          return str(build_num)
+      else:
+        first_good = build_num
+      if build_selection_policy == ALWAYS_USE_LATEST:
+        return str(build_num)
+      if not tested and build_selection_policy != SECOND_TO_LATEST:
+        return str(build_num)
+    if build_selection_policy == BACKTRACK_THEN_LATEST and first_good:
+      return str(first_good)
+  return str(BAD_BUILD_NUMBER)
+
+
+def ClaimBuildNumber(repo_alias, branch, product, flavor, build_number):
+  """Returns the good build number after claimed; or zero if fails to claim.
+
+  Claim the build by changing the zip file name
+  However, if the claim fails, we will return 0 to indicate an error.
+
+  Args:
+    repo_alias: String containing repository alias (e.g. internal/partner).
+    branch: String containing branch name (e.g. unfork_m37).
+    product: String containing product name (e.g. anchovy).
+    flavor: String containing one particular build flavor (e.g. eng).
+    build_number: String containing the build number (e.g. 19219).
+
+  Returns:
+    String: build_number value if claim is successful; otherwise, zero.
+  """
+  good_build_num = build_number
+  unit_test_root_repo = GetUnitTestGcsRootPath(product, repo_alias)
+  hint_path_pattern = UNIT_TEST_MARKER_DIR % (
+      unit_test_root_repo,
+      branch,
+      product,
+      flavor,
+      good_build_num) + '%s'
+  original_hint_path = hint_path_pattern % UNIT_TEST_MARKER
+  claimed_hint_path = hint_path_pattern % CLAIMED_UNIT_TEST_MARKER
+  mv_cmd = GS_MV_MARKER_CMD % (original_hint_path, claimed_hint_path)
+  try:
+    _Execute(mv_cmd, exit_on_stderr=True)
+  except IOError:
+    good_build_num = '0'
+  return good_build_num
+
+
+def GetLatestBuildNumber(repo_alias, branch, product,
+                         triangulation_list=DEFAULT_TRIANGULATION_LIST,
+                         flavor=BUILD_FLAVORS['eng'], board_type='',
+                         build_selection_policy=DEFAULT_BUILD_SELECTION_POLICY,
+                         max_build_selection_range=DEFAULT_MAX_BUILD_LIST_LEN):
+  """Prints latest successful build number for given branch and product.
+
+  By default, take pre-cautions by cross examining two flavors (eng and user)
+  to determine a common latest build. However, it allows to use one flavor.
+
+  Args:
+    repo_alias: String containing repository alias (e.g. internal/partner).
+    branch: String containing branch name (e.g. unfork_m37).
+    product: String containing product name (e.g. anchovy).
+    triangulation_list: Optional string containing flavor names separated
+        by spaces.
+    flavor: Optional string containing one particular build flavor (e.g. eng).
+    board_type: Optional string for device board type.
+    build_selection_policy: The policy used to select the build.
+    max_build_selection_range: The maximum number of builds to be used for
+        build selection process.
+
+  Returns:
+    String: build number value or 0 if no build found.
+  """
+  common_build_numbers = []
+
+  if product in TRIANGULATION_OVERRIDE_DICT: # pylint: disable=consider-using-get
+    triangulation_list = TRIANGULATION_OVERRIDE_DICT[product]
+
+  # Special treat eng only repositories
+  if repo_alias in ENG_ONLY_REPOSITORY_LIST:
+    triangulation_list = BUILD_FLAVORS['eng']
+
+  for internal_flavor in triangulation_list.split():
+    build_numbers = list(map(
+        ToBuildNumber,
+        GetBuildsListByFlavor(
+            repo_alias, branch, product, internal_flavor, board_type)))
+    if common_build_numbers:
+      common_build_numbers = (
+          list(set(common_build_numbers) & set(build_numbers)))
+    else:
+      common_build_numbers = build_numbers
+  int_build_numbers = list(map(int, common_build_numbers))
+  try:
+    # A good build should have good_to_test file in bigstore
+    if product in PRODUCT_ROOT_REPOSITORY:
+      cloud_storage = GetRepoGcsRootPath(product, repo_alias)
+      marker_file_name = GOOD_BUILD_MARKER
+      if FLAGS.use_unit_test_hint:
+        # Wait for random seconds (0-20) to avoid file operation collision
+        time.sleep(random.randint(0, RANDOM_BACKOFF_SECS))
+        cloud_storage = GetUnitTestGcsRootPath(product, repo_alias)
+        marker_file_name = UNIT_TEST_MARKER
+      cmd_pattern = GS_LIST_FILE % (cloud_storage,
+                                    branch,
+                                    product,
+                                    flavor,
+                                    '%s',
+                                    '%s')
+      has_been_tested_cmd = ''
+
+      # Only get latest non tested build
+      if FLAGS.test_identifier:
+        # Only match the first part of the test_identifier
+        test_name = FLAGS.test_identifier.split('#')[0]
+        has_been_tested_cmd = GS_LIST_FILE % (IDS_REPOSITORY,
+                                              branch,
+                                              product,
+                                              flavor,
+                                              '%s',
+                                              os.path.join(
+                                                  TEST_IDENTIFIER_DIR,
+                                                  test_name + '*'))
+
+      good_build_num = GetBuildNumberWithMarker(
+          int_build_numbers,
+          cmd_pattern,
+          marker_file_name,
+          has_been_tested_cmd,
+          build_selection_policy=build_selection_policy,
+          max_build_selection_range=max_build_selection_range)
+
+      if FLAGS.claim_unit_test_hint and good_build_num != BAD_BUILD_NUMBER:
+        ClaimBuildNumber(
+            repo_alias, branch, product, flavor, good_build_num)
+      return good_build_num
+    return str(int_build_numbers[-1])
+  except TypeError:
+    return BAD_BUILD_NUMBER
+
+
+def GetBuildPath(repo_alias, branch, product, build_number, build_output_type,
+                 flavor, board_type):
+  """Prints the build path for build number in branch and product specified."""
+  build_paths = _Execute(
+      GetListBuildsCommand(
+          repo_alias=repo_alias,
+          branch=branch, product=product, build_number=build_number,
+          flavor=flavor, build_output_type=build_output_type,
+          board_type=board_type)).split()
+  return build_paths[-1]
+
+
+def GetOutputPath(build_path, output_path):
+  """Returns the output path where the build file is copied.
+
+  Args:
+    build_path: String containing the bigstore build path (Eg. gs://gtv-te/...).
+    output_path: String optionally containing location to copy the build to.  If
+                 empty, a temp directory path is used.
+
+  Returns:
+    String containing the correct path where the build should be copied.
+
+  Raises:
+    IOError: when the path is empty or does not exist
+  """
+  if output_path:
+    if os.path.isdir(output_path):
+      raise IOError('Path (%s) must include file name.' % output_path)
+    return output_path
+  temp_dir = tempfile.mkdtemp()
+  return os.path.join(temp_dir, os.path.basename(build_path))
+
+
+def CopyToDir(build_path, output_path, cp_cmd_template=GS_COPY_BUILD_CMD):
+  """Copies the given bigstore build_path to the given output_path.
+
+  Args:
+    build_path: String containing the bigstore build path (Eg. gs://gtv-te/...).
+    output_path: String containing the location to copy the build to.
+    cp_cmd_template: Formatting string to interpolate to build the cp command
+
+  Returns:
+    String containing the location to copy the build to.
+  """
+  _Execute(cp_cmd_template % (build_path, output_path))
+  return output_path
+
+
+def main():
+  # For tablets, there's not really a valid buildnumber that's useful
+  # so instead we use a datetime string to specify unique runs
+  if FLAGS.product in TABLETS:
+    print(str(datetime.datetime.now().strftime('%d%B%Y%I%M%p')))
+
+  # TODO(kevinfei): we have too many products and each does not fall into the
+  # same way to retrieve build. Re-organize this script for the readability
+  # and long-term maintainability.
+  # Android TV is a prodcut which needs unit test coverage. Once a build is
+  # available, test hint (test_deps.zip) will be available in the Cloud
+  # Storage (e.g. gs://chromecast-unit-test/internal/content_shell_androidtv_
+  # x86_internal_gn/19912/test_deps.zip.
+  # To claim this build, rename the file name to test_deps_claimed.zip.
+  # When the functional test (e.g. Cast Compliance) starts, the marker file
+  # will be storage in the Cloud Storage (different bucket though) and the
+  # test will check for the existence of that file to determine if that build
+  # should be tested.
+  elif FLAGS.product in ANDROID_PRODUCTS:
+    atv_helper = AndroidTvBuildHelper(FLAGS.product, FLAGS.use_gn)
+    if FLAGS.output_mode == 'path':
+      claimed_file_path = atv_helper.GetBuildPath(
+          FLAGS.build_number,
+          FLAGS.branch,
+          FLAGS.flavor,
+          claim_hint=FLAGS.claim_unit_test_hint)
+      print(CopyToDir(claimed_file_path, FLAGS.build_output_path))
+    else:
+      latest_build = (
+          atv_helper.GetLatestBuildNumber(
+              FLAGS.branch,
+              FLAGS.flavor,
+              claim_hint=FLAGS.claim_unit_test_hint,
+              test_identifier=FLAGS.test_identifier))
+      if FLAGS.claim_unit_test_hint and latest_build != BAD_BUILD_NUMBER:
+        try:
+          atv_helper.ClaimBuildNumber(latest_build,
+                                      FLAGS.branch,
+                                      FLAGS.flavor)
+        except IOError:
+          latest_build = BAD_BUILD_NUMBER
+      if latest_build == BAD_BUILD_NUMBER:
+        exit(1)
+      else:
+        print(latest_build)
+
+  else:
+    if FLAGS.output_mode == 'path':
+      build_path = GetBuildPath(FLAGS.repo_alias, FLAGS.branch, FLAGS.product,
+                                FLAGS.build_number, FLAGS.build_output_type,
+                                FLAGS.flavor, FLAGS.board_type)
+      output_path = GetOutputPath(build_path, FLAGS.build_output_path)
+      print(CopyToDir(build_path, output_path))
+    else:
+      latest_build = GetLatestBuildNumber(
+          FLAGS.repo_alias,
+          FLAGS.branch,
+          FLAGS.product,
+          triangulation_list=FLAGS.build_triangulation_list,
+          flavor=FLAGS.flavor,
+          board_type=FLAGS.board_type,
+          build_selection_policy=FLAGS.build_selection_policy,
+          max_build_selection_range=FLAGS.max_build_selection_range)
+      if latest_build == BAD_BUILD_NUMBER:
+        exit(1)
+      else:
+        print(latest_build)
+
+
+if __name__ == '__main__':
+  FLAGS = ParseFlags()
+  main()
diff --git a/build/get_latest_build_test.py b/build/get_latest_build_test.py
new file mode 100755
index 0000000..a4d4432
--- /dev/null
+++ b/build/get_latest_build_test.py
@@ -0,0 +1,144 @@
+"""Tests for get_latest_build."""
+import unittest
+
+from build import get_latest_build
+from gunittest import gtestcase
+
+from unittest import mock
+
+
+class GetLatestBuildTest(gtestcase.GTestCase):
+
+  def setUp(self):
+    self._orig_get_builds = get_latest_build.GetBuildsListByFlavor
+    self._orig_execute = get_latest_build._Execute
+
+  def tearDown(self):
+    get_latest_build.GetBuildsListByFlavor = self._orig_get_builds
+    get_latest_build._Execute = self._orig_execute
+
+  def testToBuildNumber(self):
+    """Ensures build numbers can be parsed."""
+    self.assertEqual(
+        get_latest_build.ToBuildNumber(
+            'gs://gtv-te/internal/gtv-3.0/cosmo-tests/32908/'
+            'cosmo-tests-32908.zip'),
+        '32908')
+    self.assertEqual(
+        get_latest_build.ToBuildNumber(
+            'gs://gtv-te/internal/gtv-3.0/cosmo-eng/1234/'
+            'cosmo-eng-1234.zip'),
+        '1234')
+
+  def testGetLatestBuild(self):
+    """Ensures the latest build is returned."""
+    fmt = ('gs://gtv-eureka/internal/unfork_m37/anchovy-{1}/{0}'
+           '/anchovy-ota-{0}.zip')
+
+    get_latest_build.FLAGS = mock.Mock(test_identifier='',
+                                       use_unit_test_hint=False,
+                                       claim_unit_test_hint=False)
+    mock_build_output = {
+        'eng': [fmt.format(i, 'eng') for i in range(18307, 183012, 2)],
+        'user': [fmt.format(i, 'user') for i in [18304, 18305, 18307, 18309]]
+    }
+    get_latest_build.GetBuildsListByFlavor = (
+        lambda *args: mock_build_output.get(args[3]))
+    get_latest_build._Execute = mock.Mock(
+        side_effect=get_latest_build.GOOD_BUILD_MARKER)
+    get_latest_build.GetBuildNumberWithMarker = lambda x, *_, **__: str(max(x))
+    self.assertEqual(
+        get_latest_build.GetLatestBuildNumber('', 'unfork_m37', 'anchovy'),
+        '18309')
+    expected_build = fmt.format(18309, 'user')
+    get_latest_build._Execute = lambda x: expected_build
+    self.assertEqual(
+        get_latest_build.GetBuildPath(
+            '', 'unfork_m37', 'anchovy', '18309', 'ota', 'user', ''),
+        expected_build)
+
+  def testExecute(self):
+    """Ensures execute works."""
+    self.assertEqual('foo bar\n', get_latest_build._Execute('echo foo bar'))
+
+  def testGetListCmd(self):
+    """Ensures the correct list command is built."""
+    kwargs = {
+        'product': 'anchovy',
+        'build_output_type': 'w',
+        'board_type': 'x',
+        'branch': 'y',
+        'flavor': 'z'
+    }
+
+    self.assertEqual(
+        get_latest_build.GetListBuildsCommand(**kwargs),
+        ('gsutil ls gs://gtv-eureka/internal/y/anchovy-z/[0-9]*/'
+         'anchovy-w-x-[0-9]*.zip'))
+    kwargs['build_number'] = 'A'
+    self.assertEqual(
+        get_latest_build.GetListBuildsCommand(**kwargs),
+        'gsutil ls gs://gtv-eureka/internal/y/anchovy-z/A/anchovy-w-x-A.zip')
+    kwargs['build_output_type'] = 'fct'
+    self.assertEqual(
+        get_latest_build.GetListBuildsCommand(**kwargs),
+        'gsutil ls gs://gtv-eureka/internal/y/anchovy-z/A/factory/'
+        'anchovy-fct-x-A.zip')
+
+
+class GetBuildNumberWithMarkerTest(gtestcase.GTestCase):
+  """Tests for the GetBuildNumberWithMarker() function."""
+
+  def _TestBuildNumberWithMarker(
+      self, build_numbers, expected_build, build_selection_policy,
+      build_marked_mock=None, build_tested_mock=None):
+    """Helper to test GetBuildNumberWithMarker()."""
+    if build_marked_mock:
+      get_latest_build._IsBuildMarked = build_marked_mock
+    if build_tested_mock:
+      get_latest_build._IsBuildTested = build_tested_mock
+    latest_build = get_latest_build.GetBuildNumberWithMarker(
+        build_numbers, 'MOCK_COMMAND_PATTERN',
+        has_been_tested_cmd='MOCK_TESTED_COMMAND',
+        build_selection_policy=build_selection_policy)
+    self.assertEqual(expected_build, latest_build)
+
+  def testBacktrackThenLatestPolicy(self):
+    """Tests the first untested build is returned."""
+    self._TestBuildNumberWithMarker(
+        [3, 2, 1], '1', get_latest_build.BACKTRACK_THEN_LATEST,
+        lambda *_: True, mock.Mock(side_effect=[True, True, False]))
+    self._TestBuildNumberWithMarker(
+        [3, 2, 1], '3', get_latest_build.BACKTRACK_THEN_LATEST,
+        lambda *_: True, lambda *_: True)
+    self._TestBuildNumberWithMarker(
+        [3, 2, 1], '0', get_latest_build.BACKTRACK_THEN_LATEST,
+        lambda *_: False)
+
+  def testBacktrackOnlyPolicy(self):
+    """Tests that no build is returned when the flag is not specified."""
+    self._TestBuildNumberWithMarker(
+        [3, 2, 1], '0', get_latest_build.ONLY_BACKTRACK,
+        lambda *_: True, lambda *_: True)
+    self._TestBuildNumberWithMarker(
+        [1, 2, 3], '1', get_latest_build.ONLY_BACKTRACK,
+        lambda *_: True, mock.Mock(side_effect=[True, True, False]))
+    self._TestBuildNumberWithMarker(
+        [3, 2, 1], '0', get_latest_build.BACKTRACK_THEN_LATEST,
+        lambda *_: False)
+
+  def testAlwaysLatestPolicy(self):
+    """Tests the always return latest policy."""
+    self._TestBuildNumberWithMarker(
+        [1, 2, 3], '3', get_latest_build.ALWAYS_USE_LATEST,
+        lambda *_: True, lambda *_: False)
+    self._TestBuildNumberWithMarker(
+        [1, 2, 3], '3', get_latest_build.ALWAYS_USE_LATEST,
+        lambda *_: True, lambda *_: True)
+    self._TestBuildNumberWithMarker(
+        [3, 2, 1], '0', get_latest_build.ALWAYS_USE_LATEST,
+        lambda *_: False)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/build_server/get_ota_size.py b/build_server/get_ota_size.py
new file mode 100755
index 0000000..4399ec4
--- /dev/null
+++ b/build_server/get_ota_size.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+#
+# Copyright 2014 Google Inc. All Rights Reserved.
+
+"""Utility to get the ota size for all Chromecast builds.
+
+It will fetch the ota stored in bigstore and report the size of them in a csv.
+"""
+
+import optparse
+import re
+import subprocess
+
+# Command to list all builds.
+GET_OTA_INFO_CMD = ('gsutil ls -l gs://gtv-eureka/internal/%s/anchovy-eng/*/'
+                    'anchovy-ota-*.zip')
+GET_OTA_RE = re.compile(r'\s+([0-9]+)\s+.*anchovy-ota-([0-9]+)\.zip')
+
+
+def ParseArgs():
+  """Parse args."""
+  parser = optparse.OptionParser()
+  parser.add_option('--branch', default='merge29', help=('Branch to get ota'))
+  parser.add_option('--output_csv', default='build_size.csv', help=('Csv'))
+  options = parser.parse_args()[0]
+  return options
+
+
+def GetOtaInfo(branch, output_csv):
+  """Returns the ota size and build number in a csv file."""
+  ota_info_list = []
+  with open('/tmp/ota_info.txt', 'w') as f:
+    subprocess.call(GET_OTA_INFO_CMD % branch, shell=True, stdout=f)
+  with open('/tmp/ota_info.txt', 'r') as f:
+    ota_info_list = f.readlines()
+  if ota_info_list:
+    with open(output_csv, 'w') as f:
+      for ota in ota_info_list:
+        info = GET_OTA_RE.search(ota)
+        if info:
+          f.write(','.join((info.group(2), info.group(1)))+'\n')
+
+FLAGS = ParseArgs()
+
+
+def main():
+  GetOtaInfo(FLAGS.branch, FLAGS.output_csv)
+
+if __name__ == '__main__':
+  main()
diff --git a/build_server/push_symbols.par b/build_server/push_symbols.par
new file mode 100755
index 0000000..396a183
--- /dev/null
+++ b/build_server/push_symbols.par
Binary files differ
diff --git a/catatester/Makefile b/catatester/Makefile
new file mode 100644
index 0000000..dc9f0bf
--- /dev/null
+++ b/catatester/Makefile
@@ -0,0 +1,23 @@
+# Makefile to update protobuf objects
+#
+# Every time the protos are updated, just run:
+#   $ make
+
+CWD=$(shell pwd)
+
+PROTOFOLDER=$(CWD)/proto
+IMPORTSFOLDER=$(CWD)/proto/imported
+OUTPUT_FOLDER=$(CWD)/genfiles
+
+.PHONY: proto
+
+all: proto
+
+gen_proto:
+	protoc -I$(PROTOFOLDER) --python_out=$(OUTPUT_FOLDER) $(PROTOFOLDER)/*.proto
+	protoc -I$(IMPORTSFOLDER) --python_out=$(OUTPUT_FOLDER) $(IMPORTSFOLDER)/*.proto
+
+gen_init:
+	touch $(OUTPUT_FOLDER)/__init__.py
+
+proto: gen_proto gen_init
diff --git a/catatester/README.md b/catatester/README.md
new file mode 100644
index 0000000..4c6a215
--- /dev/null
+++ b/catatester/README.md
@@ -0,0 +1,66 @@
+# Updating the protos
+
+Just run `make` and the generated protos will be updated.
+
+If the protoc compiler is confused by the map type, you are probably
+running version of protoc < v3.0. You can check with `protoc --version`. See
+below for updating your protoc version.
+
+# Installing protoc v3 compiler
+
+Grab protoc-3.2.0-linux-x86_64.zip release from
+https://github.com/google/protobuf/releases/tag/v3.2.0
+
+Newer releases of protoc are not compatible with the current version of protobuf python package
+installed in requirements.txt at the time of this writing.
+
+Extract `bin/protoc` from the release and copy it into /usr/bin/protoc. Give it execute permissions.
+
+Check the new version with `protoc --version`.
+
+# Running unittests
+
+This section covers the process for executing Python3 unittests located under this directory.
+
+## Configuring Virtualenv
+
+1.  Download CQ package mirror.
+
+    ```
+    mkdir -p ~/envs/mirror/python_packages
+    gsutil rsync \
+        "gs://nest-build-dependencies/common/python_packages" \
+        ~/envs/mirror/python_packages
+    ```
+
+1.  Create a new virtual environment.
+
+    ```
+    python3 -m venv ~/envs/cq_venv
+    ```
+
+1.  Upgrade common modules.
+
+    ```
+    ~/envs/cq_venv/bin/pip install \
+        --no-index \
+        --find-links=file:///<PATH_TO_HOME>/envs/mirror/python_packages \
+        --upgrade pip wheel setuptools
+    ```
+
+1.  Install requirements.
+
+    ```
+    ~/envs/cq_venv/bin/pip install \
+        --no-index \
+        --find-links=file:///<PATH_TO_HOME>/envs/mirror/python_packages \
+        -r <REPO_ROOT>/continuous-tests/cq/requirements_recipe3.txt
+    ```
+
+1.  Run tests.
+
+    ```
+    ~/envs/cq_venv/bin/python3 -m unittest discover \
+        -s ~/gazoo_repo/continuous-tests/catatester \
+        -p '*_test.py'
+    ```
diff --git a/catatester/__init__.py b/catatester/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/catatester/__init__.py
diff --git a/catatester/cipd/cipd_packages.yaml b/catatester/cipd/cipd_packages.yaml
new file mode 100644
index 0000000..a96a24d
--- /dev/null
+++ b/catatester/cipd/cipd_packages.yaml
@@ -0,0 +1,17 @@
+# This file specifies the cipd package installation root directory relative to
+# the root directory.
+#
+# devicemap package contains the devices associated with each host server.
+- 'package': 'home_internal/devicemap'
+  'relative_path': 'swarm_home/devicemap'
+# swarm_puppet package contains the puppet configuration used for swarming
+# bot setup and host server monitoring.
+- 'package': 'home_internal/swarm_puppet'
+  'relative_path': 'puppet'
+# test_binaries package contains the test run binaries
+- 'package': 'home_internal/test_binaries'
+  'relative_path': 'swarm_home/eurtest_binaries/home_binaries/tests'
+# tool_binaries package contains miscellaneous binaries for device health
+# check, host monitoring and etc.
+- 'package': 'home_internal/tool_binaries'
+  'relative_path': 'swarm_home/eurtest_binaries/home_binaries/tools'
diff --git a/catatester/config_expectations_test.py b/catatester/config_expectations_test.py
new file mode 100644
index 0000000..8c9a345
--- /dev/null
+++ b/catatester/config_expectations_test.py
@@ -0,0 +1,39 @@
+"""Expectation diff tests for continuous_tests.protoascii configs."""
+
+import unittest
+
+from catatester import config_utils
+
+
+_FAILURE_MSG = (
+    '{} config does not match expectation!\n\n' +
+    'To update expectations, run:\n' +
+    '```\n' +
+    'python continuous-tests/catatester/config_expectations_update.py\n' +
+    '```')
+
+
+def _read_expectation(path):
+  with open(path) as f:
+    return f.read()
+
+
+class ConfigExpectationsTest(unittest.TestCase):
+
+  def testVerifyProdExpectation(self):
+    """Verify prod config matches expectation."""
+    message = config_utils.parse_continuous_tests_protoascii(
+        config_utils.PROD_CONFIG_PATH)
+    expectation = _read_expectation(config_utils.PROD_EXPECTATION_PATH)
+    self.assertEqual(expectation, str(message), _FAILURE_MSG.format('Prod'))
+
+  def testVerifyStagingExpectation(self):
+    """Verify staging config matches expectation."""
+    message = config_utils.parse_continuous_tests_protoascii(
+        config_utils.STAGING_CONFIG_PATH)
+    expectation = _read_expectation(config_utils.STAGING_EXPECTATION_PATH)
+    self.assertEqual(expectation, str(message), _FAILURE_MSG.format('Staging'))
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/catatester/config_expectations_update.py b/catatester/config_expectations_update.py
new file mode 100644
index 0000000..ef14e2c
--- /dev/null
+++ b/catatester/config_expectations_update.py
@@ -0,0 +1,31 @@
+"""Update continuous_tests config expected content.
+
+Expectation files will be updated with the parsed versions of the staging and
+production continuous_tests.protoascii files.
+
+These files should be used to verify config changes have had the expected
+effect.
+"""
+
+from catatester import config_utils
+
+
+def _write_expectation(message, path):
+  with open(path, 'w') as f:
+    f.write(str(message))
+
+
+def main():
+  print('Updating prod config expectation.')
+  prod_message = config_utils.parse_continuous_tests_protoascii(
+      config_utils.PROD_CONFIG_PATH)
+  _write_expectation(prod_message, config_utils.PROD_EXPECTATION_PATH)
+
+  print('Updating staging config expectation.')
+  staging_message = config_utils.parse_continuous_tests_protoascii(
+      config_utils.STAGING_CONFIG_PATH)
+  _write_expectation(staging_message, config_utils.STAGING_EXPECTATION_PATH)
+
+
+if __name__ == '__main__':
+  main()
diff --git a/catatester/config_test.py b/catatester/config_test.py
new file mode 100644
index 0000000..55b3f0f
--- /dev/null
+++ b/catatester/config_test.py
@@ -0,0 +1,304 @@
+"""Tests for tests.protoascii configs."""
+
+import abc
+import collections
+import itertools
+import os
+import re
+import unittest
+
+from catatester.genfiles import continuous_tests_pb2
+from catatester import config_utils
+
+# Import from eureka-internal/builder/masters project.
+# pylint: disable=ungrouped-imports
+try:
+  from src.genfiles import build_config_pb2
+except ImportError:
+  from catatester.genfiles import build_config_pb2
+
+import google.protobuf.text_format
+
+_CATATESTER_ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
+_CONTINUOUS_TESTS_ROOT_PATH = os.path.dirname(_CATATESTER_ROOT_PATH)
+
+_SKIP_UNIT_TEST_RE = re.compile(r'unit_test.*')
+_REQUIRED_FIELDS = ['TEST_CASE_CLASS', 'TEST_CASE_MODULE',
+                    'TEST_SEQUENCE_CLASS', 'TEST_SEQUENCE_MODULE']
+
+# Cap the execution_timeout to 23 hours (b/176174164)
+_MAX_EXECUTION_TIMEOUT_MINS = 23 * 60
+
+# Assumes full test manifest repo checkout.
+_BUILD_CONFIG_FILEPATH = os.path.join(
+    os.path.dirname(_CONTINUOUS_TESTS_ROOT_PATH), 'builder', 'masters',
+    'configs', 'eureka', 'build_config.protoascii')
+
+
+def _GetBuildConfig():
+  """Returns the build_config protoascii from the builder/masters project.
+
+  Returns:
+    Ingested build_config.
+  """
+  build_config = build_config_pb2.BuildConfig()
+  with open(_BUILD_CONFIG_FILEPATH) as f:
+    google.protobuf.text_format.Merge(f.read(), build_config)
+  return build_config
+
+
+# pylint: disable=no-member
+# Test is not directly derived from UnitTest base class.
+class ConfigTest(object):
+  """A class to test a continuous_tests config.
+
+  Note: This class will be tested twice, once with the staging config
+  and once with the prod config.
+  """
+  ASSISTANT = build_config_pb2.OtaBuild.Capability.Value(
+      'ASSISTANT')
+  CAST_VIDEO_RECEIVER = build_config_pb2.OtaBuild.Capability.Value(
+      'CAST_VIDEO_RECEIVER')
+  CAST_AUDIO_RECEIVER = build_config_pb2.OtaBuild.Capability.Value(
+      'CAST_AUDIO_RECEIVER')
+
+  def __init__(self):
+    self.__metaclass__ = abc.ABCMeta
+
+  @abc.abstractproperty
+  def tests_proto(self):
+    pass
+
+  @property
+  def build_config_proto(self):
+    return _GetBuildConfig()
+
+  @property
+  def all_tests(self):
+    return (
+        list(self.tests_proto.continuous_test) +
+        list(self.tests_proto.scheduled_test))
+
+  def testContinuousTestsConfig_IsValidProto(self):
+    """Tests the continuous_tests config is a valid proto."""
+    self.assertTrue(self.tests_proto)
+
+  def testEveryTestTarget_hasTestConfigOrTestBinary(self):
+    """Test that every test_target has a test_config or test_binary_name."""
+    for test in self.all_tests:
+      self.assertTrue(
+          test.test_target.binary_name or
+          test.test_target.config)
+
+  def testEveryScheduledTestsBuildTargetsHasProductsOrProductTypes(self):
+    """Test that scheduled test build_target has products or product types."""
+    for test in self.tests_proto.scheduled_test:
+      for target in test.build_targets:
+        msg = ('Scheduled tests require build_target.products or'
+               'build_target.product_types: %s' % test)
+        self.assertTrue(
+            target.products or
+            target.product_types, msg)
+
+  def testEveryScheduledTestHasSetACron(self):
+    """Verify every scheduled test has set a cron string."""
+    for test in self.tests_proto.scheduled_test:
+      self.assertTrue(test.schedule.cron,
+                      'scheduled_test is missing cron string: %s' % test)
+
+  def testEveryTestTargetGroupIsDefined(self):
+    """Test that all test target groups are defined in test_group_tags."""
+    for test in self.all_tests:
+      for test_group in test.test_target.test_groups:
+        self.assertIn(test_group, self.tests_proto.test_group_tags)
+
+  def testEveryTestContainsGroup(self):
+    """Ensures group is included in every test."""
+    for test in self.all_tests:
+      groups = [dim for dim in test.test_target.testbed_requirements if
+                dim.bot_dimension.lower() == 'group']
+      self.assertTrue(groups,
+                      'Missing group in target: %s' % test.test_target.config)
+
+  def testEveryTestHasValidTestTimeout(self):
+    """Ensures test_timeout_mins is under the max allowed value."""
+    for test in self.all_tests:
+      self.assertLessEqual(
+          test.test_target.test_timeout_mins,
+          _MAX_EXECUTION_TIMEOUT_MINS,
+          'Timeout mins > %s in target: %s' % (_MAX_EXECUTION_TIMEOUT_MINS,
+                                               test.test_target.config))
+
+  def testEveryTargetedTestConfigFileExists(self):
+    """Test that every targeted test_config file exists."""
+    catatester_path = os.path.abspath(os.path.dirname(__file__))
+    root_path = os.path.dirname(catatester_path)
+    test_configs_root = os.path.join(
+        root_path, self.tests_proto.test_configs_root)
+    for test in self.all_tests:
+      if test.test_target.config:
+        config_abspath = os.path.join(
+            test_configs_root,
+            test.test_target.config)
+        self.assertTrue(
+            os.path.isfile(config_abspath),
+            'test_target.config does not exist: %s' % config_abspath)
+
+  def testEveryTestConfigFileContainsRequiredFields(self):
+    """Test that every test_config file contains required fields."""
+
+    for test in self.all_tests:
+      if (not test.test_target.config
+            or re.match(_SKIP_UNIT_TEST_RE, test.test_target.config)):
+          continue
+
+      fname = os.path.join(_CONTINUOUS_TESTS_ROOT_PATH,
+              self.tests_proto.test_configs_root, test.test_target.config)
+      with open(fname) as f:
+          lines = f.readlines()
+
+          found_fields = []
+          for field in _REQUIRED_FIELDS:
+            for line in lines:
+              if not re.match('^%s.*' % field, line, re.I):
+                continue
+              _, value = re.split('=', line, 2)
+              self.assertIsNotNone( value.strip(),
+                  '%s in %s must have a value.' % (field, fname))
+              found_fields.append(field)
+
+          self.assertEqual(_REQUIRED_FIELDS, sorted(found_fields),
+              '%s must contain all of %s.' % (fname, _REQUIRED_FIELDS))
+
+  def testTarget_ValidCipdRefsKey(self):
+    """Tests targets provide valid cipd refs."""
+    cipd_ref_keys = self.tests_proto.cipd_refs.keys()
+    for test in self.all_tests:
+      msg = 'cipd_ref_key should be one of %s but got %s' % (
+          cipd_ref_keys, test.test_target.cipd_refs_key)
+      self.assertIn(test.test_target.cipd_refs_key, cipd_ref_keys, msg)
+
+  def testContinuousTestsConfigDuplicates(self):
+    """Tests there are no duplicated definitions for continuous_tests."""
+    dict_continuous_tests = collections.defaultdict(list)
+    for continuous_test in self.tests_proto.continuous_test:
+      products_set = self._GetProductsForContinuousTest(continuous_test)
+      config_variants = list(continuous_test.build_targets)[0].variants
+      testbed_reqs = self._GetTestbedRequirementsForContinuousTest(
+          continuous_test)
+      config_key = '%s-%s-%s' % (
+          continuous_test.test_target.config,
+          continuous_tests_pb2.BuildTargets.Variant.Name(config_variants),
+          ';'.join(testbed_reqs))
+      if not continuous_test.test_target.test_cases:
+        dict_continuous_tests[config_key].append(products_set)
+      else:
+        for test_case in continuous_test.test_target.test_cases:
+          config_test_case_key = '%s-%s' % (config_key, test_case)
+          dict_continuous_tests[config_test_case_key].append(products_set)
+    for config in dict_continuous_tests:
+      combinations = itertools.combinations(dict_continuous_tests[config], 2)
+      duplicates = next((c for c in combinations if set.intersection(*c)), None)
+      msg = 'Overlapping config found for %s, products %s' % (config,
+                                                              duplicates)
+      self.assertIsNone(duplicates, msg)
+
+  def _GetDevicesByCapability(self, capability_index):
+    """Getting sets of devices filtered by the index received.
+
+    Args:
+      capability_index: Number provided to filter the capabilities of devices
+
+    Returns:
+      returns the set of filtered devices
+      For example, if capability_index is CAST_VIDEO_RECEIVER:
+      {'chorizo', 'steak', 'salami'}
+    """
+    ota_dict = self.build_config_proto.ota_build
+    set_cast_products = set()
+    for key in ota_dict:
+      if ota_dict[key].capabilities:
+        if capability_index in list(ota_dict[key].capabilities):
+          set_cast_products.add(ota_dict[key].product)
+    return set_cast_products
+
+  def _GetProductsForContinuousTest(self, continuous_test):
+    """Getting set of products for the specified continuous_test.
+
+    Args:
+      continuous_test: continuous test object.
+
+    Returns:
+      returns a set with products of the continuous test
+    """
+    assistant_devices = self._GetDevicesByCapability(self.ASSISTANT)
+    cast_video_receiver = self._GetDevicesByCapability(
+        self.CAST_VIDEO_RECEIVER)
+    cast_audio_receiver = self._GetDevicesByCapability(
+        self.CAST_AUDIO_RECEIVER)
+    set_products = set()
+    set_product_types = set()
+    for build_target in continuous_test.build_targets:
+      if build_target.products:
+        set_products.update(list(build_target.products))
+      if build_target.product_types:
+        product_types = list(build_target.product_types)
+        if 'ASSISTANT' in product_types:
+          set_product_types |= assistant_devices
+        if 'CAST_AUDIO_RECEIVER' in product_types:
+          set_product_types |= cast_audio_receiver
+        if 'CAST_VIDEO_RECEIVER' in product_types:
+          set_product_types |= cast_video_receiver
+    products = set_products | set_product_types
+    return products
+
+  def _GetTestbedRequirementsForContinuousTest(self, continuous_test):
+    """Returns a list of bot dimensions required for the test."""
+    test_target = continuous_test.test_target
+    if not test_target.skip_default_dimensions:
+      return ['dims:default']
+    requirements = []
+    for req in test_target.testbed_requirements:
+      requirements.append(':'.join([req.bot_dimension, req.value]))
+    return requirements
+
+  def testEveryTestTargetBinaryNameExists(self):
+    """Test that every test_target.binary_name does exist as subdir.
+
+    Except for test_target with recipe as recipe defines subdir.
+    """
+    binaries_and_recipes = {(test.test_target.binary_name,
+                             test.test_target.recipe)
+                            for test in self.all_tests
+                            if test.test_target.binary_name}
+    for binary_name, recipe in binaries_and_recipes:
+      subdir = os.path.join(_CONTINUOUS_TESTS_ROOT_PATH,
+                            'lab_system', 'master', 'config', 'tests',
+                            binary_name)
+      subdir = os.path.abspath(subdir)
+      self.assertTrue(os.path.isdir(subdir) or recipe,
+                      msg='%s does not exist.' % subdir)
+
+
+class ProdConfigTest(unittest.TestCase, ConfigTest):
+
+  @property
+  def tests_proto(self):
+    return config_utils.parse_continuous_tests_protoascii(
+        config_utils.PROD_CONFIG_PATH)
+
+  @property
+  def build_restrictions(self):
+    return self.tests_proto.build_restrictions
+
+
+class StagingConfigTest(unittest.TestCase, ConfigTest):
+
+  @property
+  def tests_proto(self):
+    return config_utils.parse_continuous_tests_protoascii(
+        config_utils.STAGING_CONFIG_PATH)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/catatester/config_utils.py b/catatester/config_utils.py
new file mode 100644
index 0000000..36085ec
--- /dev/null
+++ b/catatester/config_utils.py
@@ -0,0 +1,29 @@
+"""Reusable utility methods for working with continuous_test configs."""
+
+import os
+
+from catatester.genfiles import continuous_tests_pb2
+from google.protobuf import text_format
+
+_ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
+
+# Path to the production continuous_tests.protoascii file.
+PROD_CONFIG_PATH = os.path.join(
+    _ROOT_DIR, 'configs', 'prod', 'continuous_tests.protoascii')
+# Path to the staging continuous_tests.protoascii file.
+STAGING_CONFIG_PATH = os.path.join(
+    _ROOT_DIR, 'configs', 'staging', 'continuous_tests.protoascii')
+# Path to the expectation file for the production config.
+PROD_EXPECTATION_PATH = os.path.join(
+    _ROOT_DIR, 'configs', 'expectations', 'prod_continuous_tests.expected')
+# Path to the expectation file for the staging config.
+STAGING_EXPECTATION_PATH = os.path.join(
+    _ROOT_DIR, 'configs', 'expectations', 'staging_continuous_tests.expected')
+
+
+def parse_continuous_tests_protoascii(path):
+  """Parse a continuous_tests config from protoascii into a proto message."""
+  message = continuous_tests_pb2.ContinuousTestsConfig()
+  with open(path) as f:
+    text_format.Merge(f.read(), message)
+  return message
diff --git a/catatester/configs/README.md b/catatester/configs/README.md
new file mode 100644
index 0000000..5765041
--- /dev/null
+++ b/catatester/configs/README.md
@@ -0,0 +1,8 @@
+# NOTE
+
+After updating the continuous_test protoascii file, a new expectation file needs
+to be generated and uploaded along with the protoascii changes.
+
+```
+$ python $DEV_HOME/continuous-tests/catatester/config_expectations_update.py
+```
diff --git a/catatester/configs/expectations/prod_continuous_tests.expected b/catatester/configs/expectations/prod_continuous_tests.expected
new file mode 100644
index 0000000..da4ef21
--- /dev/null
+++ b/catatester/configs/expectations/prod_continuous_tests.expected
@@ -0,0 +1,654 @@
+test_configs_root: "lab_system/master/config/tests/"
+continuous_test {
+  test_target {
+    config: "cycle_bitrates/fuchsia_cycle_bitrates.cfg"
+    binary_name: "cycle_bitrates"
+    recipe: FUCHSIA_RUNNER
+    testbed_requirements {
+      bot_dimension: "product"
+      value: "nelson"
+    }
+    testbed_requirements {
+      bot_dimension: "device_type"
+      value: "nelson"
+    }
+    testbed_requirements {
+      bot_dimension: "group"
+      value: "COMMON"
+    }
+    bot_pool: "gazoo-prod"
+    skip_default_dimensions: true
+  }
+  build_targets {
+    products: "astro"
+    branches {
+      name: "1.52f"
+    }
+    branches {
+      name: "master"
+    }
+  }
+}
+continuous_test {
+  test_target {
+    config: "cycle_bitrates/fuchsia_cycle_bitrates.cfg"
+    binary_name: "cycle_bitrates"
+    recipe: FUCHSIA_RUNNER
+    testbed_requirements {
+      bot_dimension: "product"
+      value: "sherlock"
+    }
+    testbed_requirements {
+      bot_dimension: "device_type"
+      value: "sherlock"
+    }
+    testbed_requirements {
+      bot_dimension: "group"
+      value: "COMMON"
+    }
+    bot_pool: "gazoo-prod"
+    skip_default_dimensions: true
+  }
+  build_targets {
+    products: "astro"
+    branches {
+      name: "1.52f"
+    }
+    branches {
+      name: "master"
+    }
+  }
+}
+continuous_test {
+  test_target {
+    config: "gqnq_contract_executor/gqnq_contract_executor_unpaired.cfg"
+    binary_name: "gqnq_contract_executor"
+    enabled: false
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "camie"
+    }
+    bot_pool: "gazoo-prod"
+  }
+  build_targets {
+    products: "gq"
+    products: "nq"
+    products: "spencer"
+    variants: ENG
+  }
+}
+continuous_test {
+  test_target {
+    config: "gqnq_contract_executor/gqnq_contract_executor_virtual_presubmit.cfg"
+    binary_name: "gqnq_contract_executor"
+    enabled: false
+    recipe: VIRTUAL_DEVICE_RUNNER
+    test_groups: "GQNQ_VIRTUAL_PRESUBMIT"
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "COMMON"
+    }
+    testbed_requirements {
+      bot_dimension: "testbed_type"
+      value: "vdm-cq"
+    }
+    bot_pool: "gazoo-staging"
+    skip_default_dimensions: true
+  }
+  build_targets {
+    products: "gq_x86_64"
+    products: "nq_x86_64"
+    products: "spencer_x86_64"
+    variants: ENG
+    branches {
+      name: "master"
+    }
+  }
+}
+continuous_test {
+  test_target {
+    config: "gqnq_contract_executor/gqnq_contract_executor_presubmit.cfg"
+    binary_name: "gqnq_contract_executor"
+    enabled: false
+    test_groups: "GQNQ_CAMERA_ENGPROD_PRESUBMIT"
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "camie-cq"
+    }
+    bot_pool: "gazoo-prod"
+  }
+  build_targets {
+    products: "gq"
+    products: "nq"
+    variants: ENG
+    branches {
+      name: "master"
+    }
+  }
+}
+continuous_test {
+  test_target {
+    test_cases: "testOnArm"
+    binary_name: "libassistant-integration-test"
+    priority: CRITICAL
+    recipe: LIBASSISTANT_RUNNER
+    test_timeout_mins: 120
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "COMMON"
+    }
+    cipd_refs_key: "libassistant"
+  }
+  build_targets {
+    branch_types: MASTER
+    products: "assistant"
+    variants: ARM
+  }
+}
+continuous_test {
+  test_target {
+    test_cases: "testOnX64"
+    binary_name: "libassistant-integration-test"
+    priority: CRITICAL
+    recipe: LIBASSISTANT_RUNNER
+    test_timeout_mins: 120
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "COMMON"
+    }
+    cipd_refs_key: "libassistant"
+  }
+  build_targets {
+    branch_types: MASTER
+    products: "assistant"
+    variants: X64
+  }
+}
+scheduled_test {
+  test_target {
+    config: "perception_latency/perception_latency.cfg"
+    test_cases: "PerceptionLatencyTestCaseMotion"
+    binary_name: "perception_latency"
+    test_timeout_mins: 120
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "perception_latency"
+    }
+    bot_pool: "gazoo-prod"
+  }
+  build_targets {
+    products: "newman"
+    variants: USER
+  }
+  schedule {
+    cron: "0 * * * *"
+    policy: LATEST_UNTESTED
+  }
+  branches: "1.44"
+  branches: "1.45"
+  branches: "1.42"
+  branches: "master"
+}
+scheduled_test {
+  test_target {
+    config: "selina/gesture_data_injection.cfg"
+    binary_name: "selina"
+    enabled: true
+    test_timeout_mins: 180
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "selina_data_injection"
+    }
+    bot_pool: "gazoo-prod"
+  }
+  build_targets {
+    products: "elaine"
+    variants: ENG
+  }
+  schedule {
+    cron: "30 */2 * * *"
+    policy: LATEST
+  }
+  branches: "master"
+  branches: "1.56"
+  branches: "1.56.1"
+}
+scheduled_test {
+  test_target {
+    config: "selina/sleep_data_injection.cfg"
+    binary_name: "selina"
+    enabled: true
+    test_timeout_mins: 780
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "sleep_ext4"
+    }
+    bot_pool: "gazoo-prod"
+  }
+  build_targets {
+    products: "elaine"
+    variants: ENG
+  }
+  schedule {
+    cron: "45 */4 * * *"
+    policy: LATEST
+  }
+  branches: "master"
+  branches: "1.56"
+  branches: "1.56.1"
+}
+scheduled_test {
+  test_target {
+    config: "sound_sensing/sound_sensing_mic_writer.cfg"
+    binary_name: "sound_sensing"
+    enabled: false
+    test_timeout_mins: 1380
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "sound_sensing_mic_writer"
+    }
+    bot_pool: "gazoo-prod"
+  }
+  build_targets {
+    products: "biggie"
+    products: "elaine"
+    products: "mushroom"
+    products: "newman"
+    products: "pineapple"
+    products: "valens"
+    products: "vento"
+    variants: ENG
+  }
+  schedule {
+    cron: "30 */8 * * *"
+    policy: LATEST
+  }
+  branches: "master"
+  branches: "1.54"
+}
+scheduled_test {
+  test_target {
+    config: "sound_sensing/sound_sensing_mic_writer_p1.cfg"
+    binary_name: "sound_sensing"
+    enabled: false
+    test_timeout_mins: 1380
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "sound_sensing_mic_writer"
+    }
+    bot_pool: "gazoo-prod"
+  }
+  build_targets {
+    products: "biggie"
+    products: "elaine"
+    products: "mushroom"
+    products: "newman"
+    products: "pineapple"
+    products: "valens"
+    products: "vento"
+    variants: ENG
+  }
+  schedule {
+    cron: "0 */12 * * *"
+    policy: LATEST
+  }
+  branches: "master"
+  branches: "1.54"
+}
+scheduled_test {
+  test_target {
+    config: "sound_sensing/sound_sensing_mic_writer_p2.cfg"
+    binary_name: "sound_sensing"
+    enabled: false
+    test_timeout_mins: 1380
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "sound_sensing_mic_writer"
+    }
+    bot_pool: "gazoo-prod"
+  }
+  build_targets {
+    products: "biggie"
+    products: "elaine"
+    products: "mushroom"
+    products: "newman"
+    products: "pineapple"
+    products: "valens"
+    products: "vento"
+    variants: ENG
+  }
+  schedule {
+    cron: "0 */12 * * *"
+    policy: LATEST
+  }
+  branches: "master"
+  branches: "1.54"
+}
+scheduled_test {
+  test_target {
+    config: "selina/sleep_chester.cfg"
+    binary_name: "selina"
+    enabled: false
+    test_timeout_mins: 780
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "sleep_positive"
+    }
+    bot_pool: "gazoo-prod"
+  }
+  build_targets {
+    products: "elaine"
+    variants: ENG
+  }
+  schedule {
+    cron: "30 0,12 * * *"
+    policy: LATEST
+  }
+  branches: "1.56.2"
+}
+scheduled_test {
+  test_target {
+    config: "selina/sleep_chester_1_1.cfg"
+    binary_name: "selina"
+    enabled: false
+    test_timeout_mins: 780
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "sleep_positive"
+    }
+    bot_pool: "gazoo-prod"
+  }
+  build_targets {
+    products: "elaine"
+    variants: ENG
+  }
+  schedule {
+    cron: "30 6,18 * * *"
+    policy: LATEST
+  }
+  branches: "1.56.2"
+}
+scheduled_test {
+  test_target {
+    config: "selina/sleep_chester_2_1.cfg"
+    binary_name: "selina"
+    enabled: false
+    test_timeout_mins: 780
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "sleep_positive"
+    }
+    bot_pool: "gazoo-prod"
+  }
+  build_targets {
+    products: "elaine"
+    variants: ENG
+  }
+  schedule {
+    cron: "30 3,15 * * *"
+    policy: LATEST
+  }
+  branches: "1.56.2"
+}
+disabled_branches: "1.39nf"
+disabled_branches: "1.39nfct"
+disabled_branches: "1.42rd"
+disabled_branches: "1.46rd"
+disabled_branches: "1.49qfct"
+build_restrictions {
+  key: "stellar_v2"
+  value {
+    allowed_branch_types: MASTER
+    allowed_branch_types: RELEASE
+    allowed_variants: USER
+  }
+}
+test_group_tags: "OTA_SMOKE"
+test_group_tags: "UNIT_TESTS"
+test_group_tags: "RECONNECT_TEST"
+test_group_tags: "GQNQ_CAMERA_ENGPROD_PRESUBMIT"
+test_group_tags: "GQNQ_VIRTUAL_PRESUBMIT"
+device_modules {
+  key: "anchovy"
+  value {
+    device_module: "eurtest.device.device"
+    device_class: "Device"
+  }
+}
+device_modules {
+  key: "assistant_arm"
+  value {
+    device_module: "udm.device.raspberry_pi"
+    device_class: "RaspberryPi"
+  }
+}
+device_modules {
+  key: "assistant_x64"
+  value {
+    device_module: "udm.device.assistant_x64"
+    device_class: "AssistantX64"
+  }
+}
+device_modules {
+  key: "assistantdefault"
+  value {
+    device_module: "eurtest.device.assistant_device"
+    device_class: "Assistant"
+  }
+}
+device_modules {
+  key: "astro"
+  value {
+    device_module: "udm.device.astro"
+    device_class: "Astro"
+  }
+}
+device_modules {
+  key: "astro-assistant"
+  value {
+    device_module: "eurtest.device.fuchsia_assistant_device"
+    device_class: "FuchsiaAssistant"
+  }
+}
+device_modules {
+  key: "astro-migration"
+  value {
+    device_module: "udm.device.astro_migration"
+    device_class: "AstroMigration"
+  }
+}
+device_modules {
+  key: "biggie"
+  value {
+    device_module: "eurtest.device.assistant_device"
+    device_class: "Assistant"
+  }
+}
+device_modules {
+  key: "caprica"
+  value {
+    device_module: "eurtest.device.device"
+    device_class: "Device"
+  }
+}
+device_modules {
+  key: "chorizo"
+  value {
+    device_module: "eurtest.device.device"
+    device_class: "Device"
+  }
+}
+device_modules {
+  key: "elaine"
+  value {
+    device_module: "eurtest.device.assistant_device"
+    device_class: "Assistant"
+  }
+}
+device_modules {
+  key: "gq"
+  value {
+    device_module: "udm.device.gq_nq"
+    device_class: "GqNqDevice"
+  }
+}
+device_modules {
+  key: "gq_x86_64"
+  value {
+    device_module: "udm.device.virtual_device"
+    device_class: "VirtualDevice"
+  }
+}
+device_modules {
+  key: "mushroom"
+  value {
+    device_module: "eurtest.device.assistant_device"
+    device_class: "Assistant"
+  }
+}
+device_modules {
+  key: "nelson"
+  value {
+    device_module: "udm.device.sherlock"
+    device_class: "Sherlock"
+  }
+}
+device_modules {
+  key: "newman"
+  value {
+    device_module: "eurtest.device.assistant_device"
+    device_class: "Assistant"
+  }
+}
+device_modules {
+  key: "nq"
+  value {
+    device_module: "udm.device.gq_nq"
+    device_class: "GqNqDevice"
+  }
+}
+device_modules {
+  key: "nq_x86_64"
+  value {
+    device_module: "udm.device.virtual_device"
+    device_class: "VirtualDevice"
+  }
+}
+device_modules {
+  key: "pepperoni"
+  value {
+    device_module: "eurtest.device.chromecast_audio"
+    device_class: "ChromecastAudio"
+  }
+}
+device_modules {
+  key: "pineapple"
+  value {
+    device_module: "eurtest.device.assistant_device"
+    device_class: "Assistant"
+  }
+}
+device_modules {
+  key: "prince"
+  value {
+    device_module: "eurtest.device.assistant_device"
+    device_class: "Assistant"
+  }
+}
+device_modules {
+  key: "salami"
+  value {
+    device_module: "eurtest.device.device"
+    device_class: "Device"
+  }
+}
+device_modules {
+  key: "sherlock"
+  value {
+    device_module: "udm.device.sherlock"
+    device_class: "Sherlock"
+  }
+}
+device_modules {
+  key: "sherlock-assistant"
+  value {
+    device_module: "eurtest.device.fuchsia_assistant_device"
+    device_class: "FuchsiaAssistant"
+  }
+}
+device_modules {
+  key: "soba_emulator"
+  value {
+    device_module: "udm.device.soba"
+    device_class: "Soba"
+  }
+}
+device_modules {
+  key: "spencer"
+  value {
+    device_module: "udm.device.nest_iot_device"
+    device_class: "SpencerDevice"
+  }
+}
+device_modules {
+  key: "spencer_x86_64"
+  value {
+    device_module: "udm.device.virtual_device"
+    device_class: "VirtualDevice"
+  }
+}
+device_modules {
+  key: "steak"
+  value {
+    device_module: "eurtest.device.device"
+    device_class: "Device"
+  }
+}
+device_modules {
+  key: "udon_emulator"
+  value {
+    device_module: "udm.device.udon"
+    device_class: "Udon"
+  }
+}
+device_modules {
+  key: "valens"
+  value {
+    device_module: "eurtest.device.assistant_device"
+    device_class: "Assistant"
+  }
+}
+device_modules {
+  key: "vento"
+  value {
+    device_module: "eurtest.device.assistant_device"
+    device_class: "Assistant"
+  }
+}
+device_modules {
+  key: "venus"
+  value {
+    device_module: "udm.device.nest_iot_device"
+    device_class: "SpencerDevice"
+  }
+}
+cipd_refs {
+  key: "internal"
+  value {
+    refs {
+      package_name: "home_internal/recipes"
+      version: ""
+      path: "."
+    }
+  }
+}
+cipd_refs {
+  key: "libassistant"
+  value {
+    refs {
+      package_name: "home_internal/libassistant_tools"
+      version: ""
+      path: "."
+    }
+  }
+}
diff --git a/catatester/configs/expectations/staging_continuous_tests.expected b/catatester/configs/expectations/staging_continuous_tests.expected
new file mode 100644
index 0000000..a56007f
--- /dev/null
+++ b/catatester/configs/expectations/staging_continuous_tests.expected
@@ -0,0 +1,318 @@
+test_configs_root: "lab_system/master/config/tests/"
+continuous_test {
+  test_target {
+    config: "ota_installable/ota_installable.cfg"
+    binary_name: "ota_installable"
+    priority: CRITICAL
+    recipe: OTA_INSTALLABLE_RUNNER
+    test_groups: "OTA_SMOKE"
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "COMMON"
+    }
+  }
+  build_targets {
+    branch_types: MASTER
+    products: "prince"
+    products: "valens"
+    variants: ENG
+  }
+}
+continuous_test {
+  test_target {
+    config: "crash_loop/crash_loop.cfg"
+    binary_name: "crash_loop"
+    priority: CRITICAL
+    recipe: OTA_INSTALLABLE_RUNNER
+    test_groups: "OTA_SMOKE"
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "COMMON"
+    }
+  }
+  build_targets {
+    branch_types: MASTER
+    products: "prince"
+    products: "valens"
+    variants: ENG
+  }
+}
+continuous_test {
+  test_target {
+    config: "cast/receiver.cfg"
+    test_cases: "CastReceiverCapabilityTestCase"
+    binary_name: "cast"
+    enabled: false
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "COMMON"
+    }
+  }
+  build_targets {
+    branch_types: MASTER
+    variants: USER
+    builder_names: "steak-yeti-user"
+  }
+}
+continuous_test {
+  test_target {
+    config: "reconnect_test/reconnect_test.cfg"
+    binary_name: "reconnect_test"
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "COMMON"
+    }
+  }
+  build_targets {
+    branch_types: MASTER
+    products: "newman"
+    variants: ENG
+  }
+}
+continuous_test {
+  test_target {
+    config: "ndm_integration/ndm_integration.cfg"
+    binary_name: "ndm_integration"
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "COMMON"
+    }
+  }
+  build_targets {
+    branch_types: MASTER
+    products: "newman"
+    variants: USER
+  }
+}
+continuous_test {
+  test_target {
+    config: "fakeoculus_reconnect_proxy/fakeoculus_reconnect_proxy.cfg"
+    binary_name: "fakeoculus_reconnect_proxy"
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "COMMON"
+    }
+  }
+  build_targets {
+    branch_types: MASTER
+    products: "newman"
+    variants: USER
+  }
+}
+continuous_test {
+  test_target {
+    config: "cast_video/video_encode_accelerator.cfg"
+    binary_name: "cast_video"
+    test_timeout_mins: 120
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "COMMON"
+    }
+  }
+  build_targets {
+    branch_types: MASTER
+    products: "newman"
+    variants: USER
+  }
+}
+continuous_test {
+  test_target {
+    config: "cast_video/video_decode_accelerator.cfg"
+    binary_name: "cast_video"
+    test_timeout_mins: 120
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "COMMON"
+    }
+  }
+  build_targets {
+    branch_types: MASTER
+    products: "newman"
+    variants: USER
+  }
+}
+continuous_test {
+  test_target {
+    test_cases: "testOnX64"
+    binary_name: "libassistant-integration-test"
+    priority: CRITICAL
+    recipe: LIBASSISTANT_RUNNER
+    test_timeout_mins: 120
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "COMMON"
+    }
+    cipd_refs_key: "libassistant"
+  }
+  build_targets {
+    branch_types: MASTER
+    products: "assistant"
+    variants: X64
+  }
+}
+continuous_test {
+  test_target {
+    config: "thread/thread.cfg"
+    binary_name: "thread"
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "COMMON"
+    }
+  }
+  build_targets {
+    branch_types: MASTER
+    products: "newman"
+    variants: USER
+  }
+}
+scheduled_test {
+  test_target {
+    config: "assistant_query_latency/canonical_queries_latency_dogfood.cfg"
+    binary_name: "assistant_query_latency"
+    enabled: false
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "chirp_latency"
+    }
+  }
+  build_targets {
+    branch_types: MASTER
+    products: "pineapple"
+    variants: USER
+  }
+  schedule {
+    cron: "*/30 * * * *"
+    policy: LATEST
+  }
+}
+scheduled_test {
+  test_target {
+    config: "manhattan_ui_perf/opal_back_gestures.cfg"
+    binary_name: "manhattan_ui_perf"
+    test_timeout_mins: 30
+    testbed_requirements {
+      bot_dimension: "GROUP"
+      value: "COMMON"
+    }
+    cipd_refs_key: "internal_with_opal_dart"
+  }
+  build_targets {
+    branch_types: MASTER
+    products: "newman"
+    variants: USER
+  }
+  schedule {
+    cron: "0 0 * * *"
+    policy: LATEST
+  }
+}
+branch_type_priorities: RELEASE
+branch_type_priorities: MASTER
+disabled_branches: "1.39nf"
+disabled_branches: "1.39nfct"
+disabled_branches: "1.42rd"
+test_group_tags: "OTA_SMOKE"
+test_group_tags: "UNIT_TESTS"
+device_modules {
+  key: "assistant_arm"
+  value {
+    device_module: "udm.device.raspberry_pi"
+    device_class: "RaspberryPi"
+  }
+}
+device_modules {
+  key: "assistant_x64"
+  value {
+    device_module: "udm.device.assistant_x64"
+    device_class: "AssistantX64"
+  }
+}
+device_modules {
+  key: "caprica"
+  value {
+    device_module: "eurtest.device.device"
+    device_class: "Device"
+  }
+}
+device_modules {
+  key: "mushroom"
+  value {
+    device_module: "eurtest.device.assistant_device"
+    device_class: "Assistant"
+  }
+}
+device_modules {
+  key: "newman"
+  value {
+    device_module: "eurtest.device.assistant_device"
+    device_class: "Assistant"
+  }
+}
+device_modules {
+  key: "pepperoni"
+  value {
+    device_module: "eurtest.device.chromecast_audio"
+    device_class: "ChromecastAudio"
+  }
+}
+device_modules {
+  key: "pineapple"
+  value {
+    device_module: "eurtest.device.assistant_device"
+    device_class: "Assistant"
+  }
+}
+device_modules {
+  key: "sabrina"
+  value {
+    device_module: "eurtest.device.android"
+    device_class: "Android"
+  }
+}
+device_modules {
+  key: "vento"
+  value {
+    device_module: "eurtest.device.assistant_device"
+    device_class: "Assistant"
+  }
+}
+device_modules {
+  key: "x86_worker"
+  value {
+  }
+}
+cipd_refs {
+  key: "internal"
+  value {
+    refs {
+      package_name: "home_internal/recipes"
+      version: ""
+      path: "."
+    }
+  }
+}
+cipd_refs {
+  key: "internal_with_opal_dart"
+  value {
+    refs {
+      package_name: "home_internal/recipes"
+      version: ""
+      path: "."
+    }
+    refs {
+      package_name: "home_internal/experimental/flutter_bridge"
+      version: "latest"
+      path: "."
+    }
+  }
+}
+cipd_refs {
+  key: "libassistant"
+  value {
+    refs {
+      package_name: "home_internal/libassistant_tools"
+      version: ""
+      path: "."
+    }
+  }
+}
diff --git a/catatester/configs/imported/README.md b/catatester/configs/imported/README.md
new file mode 100644
index 0000000..dbd771d
--- /dev/null
+++ b/catatester/configs/imported/README.md
@@ -0,0 +1,11 @@
+# Usage
+
+build_config.protoascii is an imported file used only for config validation unittesting.
+
+It is imported using copybara:  
+https://eureka-internal.git.corp.google.com/continuous-tests/+/master/catatester/proto/copy.bara.sky
+
+All updates should instead be made to the original file located at:  
+https://eureka-internal.git.corp.google.com/builder/masters/+/refs/heads/master/configs/eureka/build_config.protoascii
+
+Changes made under builder/master will take effect shortly after the commit is merged
diff --git a/catatester/configs/imported/build_config.protoascii b/catatester/configs/imported/build_config.protoascii
new file mode 100644
index 0000000..59b916d
--- /dev/null
+++ b/catatester/configs/imported/build_config.protoascii
@@ -0,0 +1,709 @@
+# -*- mode: protobuffer; -*-
+# proto-file: src/proto/build_config.proto
+# proto-message: BuildConfig
+#
+# Warning:
+#  Do not record capabilities for unreleased products, after a product is released
+#  it's safe to use capabilities.
+
+# Removing capabilities for anchovy devices  to run only Push on Green tests
+# on this platform b/79264258
+ota_build {
+  key: "anchovy-eng"
+  value: {
+    build_target: "anchovy-eng"
+    product: "anchovy"
+    variant: "eng"
+    platform: "eureka"
+    test: true
+  }
+}
+
+# Removing capabilities for anchovy devices  to run only Push on Green tests
+# on this platform b/79264258
+ota_build {
+  key: "anchovy-user"
+  value: {
+    build_target: "anchovy-user"
+    product: "anchovy"
+    variant: "user"
+    platform: "eureka"
+    test: true
+  }
+}
+
+ota_build {
+  key: "assistantdefault-eng"
+  value: {
+    build_target: "assistantdefault-eng"
+    product: "assistantdefault"
+    variant: "eng"
+    platform: "assistantdefault"
+    test: true
+  }
+}
+
+ota_build {
+  key: "assistantdefault-user"
+  value: {
+    build_target: "assistantdefault-user"
+    product: "assistantdefault"
+    variant: "user"
+    platform: "assistantdefault"
+    test: true
+  }
+}
+
+ota_build {
+  key: "assistant-arm"
+  value: {
+    build_target: "assistant-arm"
+    product: "assistant"
+    variant: "arm"
+    platform: "assistant"
+    test: true
+  }
+}
+
+ota_build {
+  key: "assistant-x64"
+  value: {
+    build_target: "assistant-x64"
+    product: "assistant"
+    variant: "x64"
+    platform: "assistant"
+    test: true
+  }
+}
+
+ota_build: {
+  key: "biggie-eng"
+  value: {
+    build_target: "biggie-eng"
+    product: "biggie"
+    variant: "eng"
+    platform: "biggie"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "biggie-user"
+  value: {
+    build_target: "biggie-user"
+    product: "biggie"
+    variant: "user"
+    platform: "biggie"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "biggie-mesh-eng"
+  value: {
+    build_target: "biggie-eng"
+    product: "biggie"
+    variant: "eng"
+    platform: "biggie"
+    partner_bucket: "cast-partner-amlogic-internal"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+  }
+}
+
+ota_build: {
+  key: "biggie-mesh-user"
+  value: {
+    build_target: "biggie-user"
+    product: "biggie"
+    variant: "user"
+    platform: "biggie"
+    partner_bucket: "cast-partner-amlogic-internal"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+  }
+}
+
+ota_build: {
+  key: "caprica-eng"
+  value: {
+    build_target: "caprica-eng"
+    product: "caprica"
+    variant: "eng"
+    platform: "caprica"
+    capabilities: [CAST_VIDEO_RECEIVER, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "caprica-user"
+  value: {
+    build_target: "caprica-user"
+    product: "caprica"
+    variant: "user"
+    platform: "caprica"
+    capabilities: [CAST_VIDEO_RECEIVER, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "caprica-yeti-eng"
+  value: {
+    build_target: "caprica-eng"
+    product: "caprica"
+    variant: "eng"
+    platform: "caprica"
+    capabilities: [CAST_VIDEO_RECEIVER, WIFI_2GHZ, WIFI_5GHZ]
+  }
+}
+
+ota_build: {
+  key: "caprica-yeti-user"
+  value: {
+    build_target: "caprica-user"
+    product: "caprica"
+    variant: "user"
+    platform: "caprica"
+    capabilities: [CAST_VIDEO_RECEIVER, WIFI_2GHZ, WIFI_5GHZ]
+  }
+}
+
+ota_build: {
+  key: "chorizo-eng"
+  value: {
+    build_target: "chorizo-eng"
+    product: "chorizo"
+    variant: "eng"
+    platform: "earth"
+    capabilities: [CAST_VIDEO_RECEIVER, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "chorizo-user"
+  value: {
+    build_target: "chorizo-user"
+    product: "chorizo"
+    variant: "user"
+    platform: "earth"
+    capabilities: [CAST_VIDEO_RECEIVER, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "elaine-eng"
+  value: {
+    build_target: "elaine-eng"
+    product: "elaine"
+    variant: "eng"
+    platform: "elaine"
+    capabilities: [CAST_VIDEO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "elaine-user"
+  value: {
+    build_target: "elaine-user"
+    product: "elaine"
+    variant: "user"
+    platform: "elaine"
+    capabilities: [CAST_VIDEO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "estelle-eng"
+  value: {
+    build_target: "estelle-eng"
+    product: "estelle"
+    variant: "eng"
+    platform: "estelle"
+    capabilities: [CAST_VIDEO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "estelle-user"
+  value: {
+    build_target: "estelle-user"
+    product: "estelle"
+    variant: "user"
+    platform: "estelle"
+    capabilities: [CAST_VIDEO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "estelle-fuchsia-eng"
+  value: {
+    build_target: "estelle-fuchsia-eng"
+    product: "astro-migration"
+    variant: "eng"
+    platform: "fuchsia"
+    test: true
+  }
+}
+
+ota_build: {
+  key: "estelle-gallium-eng"
+  value: {
+    build_target: "estelle-eng"
+    product: "estelle"
+    variant: "eng"
+    platform: "estelle"
+    capabilities: [CAST_VIDEO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+  }
+}
+
+ota_build: {
+  key: "estelle-gallium-user"
+  value: {
+    build_target: "estelle-user"
+    product: "estelle"
+    variant: "user"
+    platform: "estelle"
+    capabilities: [CAST_VIDEO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+  }
+}
+
+ota_build: {
+  key: "estelle-opal-eng"
+  value: {
+    build_target: "estelle-eng"
+    product: "estelle"
+    variant: "eng"
+    platform: "estelle"
+    capabilities: [CAST_VIDEO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "estelle-opal-user"
+  value: {
+    build_target: "estelle-user"
+    product: "estelle"
+    variant: "user"
+    platform: "estelle"
+    capabilities: [CAST_VIDEO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "arm64_fuchsia"
+  value: {
+    build_target: "arm64_fuchsia"
+    product: "astro"
+    variant: "arm64"
+    platform: "fuchsia"
+    test: true
+  }
+}
+
+ota_build: {
+  key: "gq-eng"
+  value: {
+    build_target: "gq-eng"
+    product: "gq"
+    variant: "eng"
+    platform: "gq"
+    test: false
+  }
+}
+
+ota_build: {
+  key: "gq-user"
+  value: {
+    build_target: "gq-user"
+    product: "gq"
+    variant: "user"
+    platform: "gq"
+    test: false
+  }
+}
+
+ota_build: {
+  key: "mushroom-eng"
+  value: {
+    build_target: "mushroom-eng"
+    product: "mushroom"
+    variant: "eng"
+    platform: "joplin"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "mushroom-user"
+  value: {
+    build_target: "mushroom-user"
+    product: "mushroom"
+    variant: "user"
+    platform: "joplin"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "mushroom-concierge-user"
+  value: {
+    build_target: "mushroom-concierge-user"
+    product: "mushroom"
+    variant: "user"
+    platform: "joplin"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "mushroom-sdm-eng"
+  value: {
+    build_target: "mushroom-eng"
+    product: "mushroom"
+    variant: "eng"
+    platform: "joplin"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+  }
+}
+
+ota_build: {
+  key: "mushroom-sdm-user"
+  value: {
+    build_target: "mushroom-user"
+    product: "mushroom"
+    variant: "user"
+    platform: "joplin"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+  }
+}
+
+ota_build: {
+  key: "newman-eng"
+  value: {
+    build_target: "newman-eng"
+    product: "newman"
+    variant: "eng"
+    platform: "newman"
+    capabilities: [CAST_VIDEO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "newman-user"
+  value: {
+    build_target: "newman-user"
+    product: "newman"
+    variant: "user"
+    platform: "newman"
+    capabilities: [CAST_VIDEO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "newman-opal-eng"
+  value: {
+    build_target: "newman-eng"
+    product: "newman"
+    variant: "eng"
+    platform: "newman"
+    test: true
+  }
+}
+
+ota_build: {
+  key: "newman-opal-user"
+  value: {
+    build_target: "newman-user"
+    product: "newman"
+    variant: "user"
+    platform: "newman"
+    test: true
+  }
+}
+
+ota_build: {
+  key: "newman-mesh-eng"
+  value: {
+    build_target: "newman-eng"
+    product: "newman"
+    variant: "eng"
+    platform: "newman"
+  }
+}
+
+ota_build: {
+  key: "newman-mesh-user"
+  value: {
+    build_target: "newman-user"
+    product: "newman"
+    variant: "user"
+    platform: "newman"
+  }
+}
+
+ota_build: {
+  key: "nq-eng"
+  value: {
+    build_target: "nq-eng"
+    product: "nq"
+    variant: "eng"
+    platform: "nq"
+    test: false
+  }
+}
+
+ota_build: {
+  key: "nq-user"
+  value: {
+    build_target: "nq-user"
+    product: "nq"
+    variant: "user"
+    platform: "nq"
+    test: false
+  }
+}
+
+ota_build: {
+  key: "pepperoni-eng"
+  value: {
+    build_target: "pepperoni-eng"
+    product: "pepperoni"
+    variant: "eng"
+    platform: "hendrix"
+    capabilities: [CAST_AUDIO_RECEIVER, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "pepperoni-user"
+  value: {
+    build_target: "pepperoni-user"
+    product: "pepperoni"
+    variant: "user"
+    platform: "hendrix"
+    capabilities: [CAST_AUDIO_RECEIVER, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "pineapple-eng"
+  value: {
+    build_target: "pineapple-eng"
+    product: "pineapple"
+    variant: "eng"
+    platform: "chirp"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "pineapple-user"
+  value: {
+    build_target: "pineapple-user"
+    product: "pineapple"
+    variant: "user"
+    platform: "chirp"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "pineapple-sdm-eng"
+  value: {
+    build_target: "pineapple-eng"
+    product: "pineapple"
+    variant: "eng"
+    platform: "chirp"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+  }
+}
+
+ota_build: {
+  key: "pineapple-sdm-user"
+  value: {
+    build_target: "pineapple-user"
+    product: "pineapple"
+    variant: "user"
+    platform: "chirp"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+  }
+}
+
+ota_build {
+  key: "prince-eng"
+  value: {
+    build_target: "prince-eng"
+    product: "prince"
+    variant: "eng"
+    platform: "prince"
+    test: true
+  }
+}
+
+ota_build {
+  key: "prince-user"
+  value: {
+    build_target: "prince-user"
+    product: "prince"
+    variant: "user"
+    platform: "prince"
+    test: true
+  }
+}
+
+ota_build: {
+  key: "salami-eng"
+  value: {
+    build_target: "salami-eng"
+    product: "salami"
+    variant: "eng"
+    platform: "lexx"
+    capabilities: [CAST_VIDEO_RECEIVER, WIFI_2GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "salami-user"
+  value: {
+    build_target: "salami-user"
+    product: "salami"
+    variant: "user"
+    platform: "lexx"
+    capabilities: [CAST_VIDEO_RECEIVER, WIFI_2GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "steak-eng"
+  value: {
+    build_target: "steak-eng"
+    product: "steak"
+    variant: "eng"
+    platform: "luther"
+    capabilities: [CAST_VIDEO_RECEIVER, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "steak-user"
+  value: {
+    build_target: "steak-user"
+    product: "steak"
+    variant: "user"
+    platform: "luther"
+    capabilities: [CAST_VIDEO_RECEIVER, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build: {
+  key: "stellar_v2-eng"
+  value: {
+    build_target: "stellar_v2-eng"
+    product: "stellar_v2"
+    variant: "eng"
+    platform: "mt85xx"
+  }
+}
+
+ota_build: {
+  key: "stellar_v2-user"
+  value: {
+    build_target: "stellar_v2-user"
+    product: "stellar_v2"
+    variant: "user"
+    platform: "mt85xx"
+  }
+}
+
+ota_build: {
+  key: "tvdefault-eng"
+  value: {
+    build_target: "tvdefault-eng"
+    product: "tvdefault"
+    variant: "eng"
+    platform: "tvdefault"
+    test: false
+  }
+}
+
+ota_build: {
+  key: "tvdefault-user"
+  value: {
+    build_target: "tvdefault-user"
+    product: "tvdefault"
+    variant: "user"
+    platform: "tvdefault"
+    test: false
+  }
+}
+
+ota_build {
+  key: "valens-eng"
+  value: {
+    build_target: "valens-eng"
+    product: "valens"
+    variant: "eng"
+    platform: "valens"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build {
+  key: "valens-user"
+  value: {
+    build_target: "valens-user"
+    product: "valens"
+    variant: "user"
+    platform: "valens"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build {
+  key: "vento-eng"
+  value: {
+    build_target: "vento-eng"
+    product: "vento"
+    variant: "eng"
+    platform: "vento"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
+
+ota_build {
+  key: "vento-user"
+  value: {
+    build_target: "vento-user"
+    product: "vento"
+    variant: "user"
+    platform: "vento"
+    capabilities: [CAST_AUDIO_RECEIVER, ASSISTANT, WIFI_2GHZ, WIFI_5GHZ]
+    test: true
+  }
+}
diff --git a/catatester/configs/prod/continuous_tests.protoascii b/catatester/configs/prod/continuous_tests.protoascii
new file mode 100644
index 0000000..1197c88
--- /dev/null
+++ b/catatester/configs/prod/continuous_tests.protoascii
@@ -0,0 +1,682 @@
+# -*- mode: protobuffer; -*-
+#
+# proto-file: catatester/proto/continuous_tests.proto
+# proto-message: ContinuousTestsConfig
+
+#############################################
+## CIPD packages associated with a repo    ##
+#############################################
+# Different repos require different recipes
+# bundled in cipd packages. If version is empty
+# then it will replaced by the reference tag
+# provided by the controller.
+cipd_refs {
+  key: "libassistant"
+  value: {
+    refs: [
+      {
+        package_name: "home_internal/libassistant_tools"
+        version: ""
+        path: "."
+      }
+    ]
+  }
+}
+cipd_refs {
+  key: "internal"
+  value: {
+    refs: [
+      {
+        package_name: "home_internal/recipes"
+        version: ""
+        path: "."
+      }
+    ]
+  }
+}
+
+###############################################
+## Build Restrictions for specific products  ##
+###############################################
+#  Useful for new products where we don't have enough inventory to test every
+#  build_target permutation desired by the tests.
+#  Use this to set which branch_types and variants you want to allow testing
+#  for a given product.
+
+build_restrictions {
+  key: "stellar_v2"
+  value: {
+    allowed_branch_types: [MASTER, RELEASE]
+    allowed_variants: USER
+  }
+}
+
+#####################
+## Device modules  ##
+#####################
+# Different devices require different classes to control them. If the device
+# product is not defined in this map the following defaults will be used:
+#   device_module = eurtest.device.device
+#   device_class = Device
+device_modules {
+  key: "anchovy"
+  value: {
+    device_module: 'eurtest.device.device'
+    device_class: 'Device'
+  }
+}
+
+device_modules {
+  key: "assistantdefault"
+  value: {
+    device_module: 'eurtest.device.assistant_device'
+    device_class: 'Assistant'
+  }
+}
+
+device_modules {
+  key: "assistant_arm"
+  value: {
+    device_module: 'udm.device.raspberry_pi'
+    device_class: 'RaspberryPi'
+  }
+}
+
+device_modules {
+  key: "assistant_x64"
+  value: {
+    device_module: 'udm.device.assistant_x64'
+    device_class: 'AssistantX64'
+  }
+}
+
+device_modules {
+  key: "astro"
+  value: {
+    device_module: 'udm.device.astro'
+    device_class: 'Astro'
+  }
+}
+
+device_modules {
+  key: "astro-assistant"
+  value: {
+    device_module: 'eurtest.device.fuchsia_assistant_device'
+    device_class: 'FuchsiaAssistant'
+  }
+}
+
+device_modules {
+  key: "astro-migration"
+  value: {
+    device_module: 'udm.device.astro_migration'
+    device_class: 'AstroMigration'
+  }
+}
+
+device_modules {
+  key: "biggie"
+  value: {
+    device_module: 'eurtest.device.assistant_device'
+    device_class: 'Assistant'
+  }
+}
+
+device_modules {
+  key: "caprica"
+  value: {
+    device_module: 'eurtest.device.device'
+    device_class: 'Device'
+  }
+}
+
+device_modules {
+  key: "chorizo"
+  value: {
+    device_module: 'eurtest.device.device'
+    device_class: 'Device'
+  }
+}
+
+device_modules {
+  key: "elaine"
+  value: {
+    device_module: 'eurtest.device.assistant_device'
+    device_class: 'Assistant'
+  }
+}
+
+device_modules {
+  key: "gq"
+  value: {
+    device_module: 'udm.device.gq_nq'
+    device_class: 'GqNqDevice'
+  }
+}
+
+device_modules {
+  key: "gq_x86_64"
+  value: {
+    device_module: 'udm.device.virtual_device'
+    device_class: 'VirtualDevice'
+  }
+}
+
+device_modules {
+  key: "mushroom"
+  value: {
+    device_module: 'eurtest.device.assistant_device'
+    device_class: 'Assistant'
+  }
+}
+
+device_modules {
+  key: "newman"
+  value: {
+    device_module: 'eurtest.device.assistant_device'
+    device_class: 'Assistant'
+  }
+}
+
+device_modules {
+  key: "nq"
+  value: {
+    device_module: 'udm.device.gq_nq'
+    device_class: 'GqNqDevice'
+  }
+}
+
+device_modules {
+  key: "nq_x86_64"
+  value: {
+    device_module: 'udm.device.virtual_device'
+    device_class: 'VirtualDevice'
+  }
+}
+
+device_modules {
+  key: "pepperoni"
+  value: {
+    device_module: 'eurtest.device.chromecast_audio'
+    device_class: 'ChromecastAudio'
+  }
+}
+
+device_modules {
+  key: "pineapple"
+  value: {
+    device_module: 'eurtest.device.assistant_device'
+    device_class: 'Assistant'
+  }
+}
+
+device_modules {
+  key: "prince"
+  value: {
+    device_module: 'eurtest.device.assistant_device'
+    device_class: 'Assistant'
+  }
+}
+
+device_modules {
+  key: "salami"
+  value: {
+    device_module: 'eurtest.device.device'
+    device_class: 'Device'
+  }
+}
+
+device_modules {
+  key: "sherlock"
+  value: {
+    device_module: 'udm.device.sherlock'
+    device_class: 'Sherlock'
+  }
+}
+
+device_modules {
+  key: "nelson"
+  value: {
+    device_module: 'udm.device.sherlock'
+    device_class: 'Sherlock'
+  }
+}
+
+device_modules {
+  key: "sherlock-assistant"
+  value: {
+    device_module: 'eurtest.device.fuchsia_assistant_device'
+    device_class: 'FuchsiaAssistant'
+  }
+}
+
+device_modules {
+  key: "soba_emulator"
+  value: {
+    device_module: 'udm.device.soba'
+    device_class: 'Soba'
+  }
+}
+
+device_modules {
+  key: "spencer"
+  value: {
+    device_module: 'udm.device.nest_iot_device'
+    device_class: 'SpencerDevice'
+  }
+}
+
+device_modules {
+  key: "spencer_x86_64"
+  value: {
+    device_module: 'udm.device.virtual_device'
+    device_class: 'VirtualDevice'
+  }
+}
+
+device_modules {
+  key: "steak"
+  value: {
+    device_module: 'eurtest.device.device'
+    device_class: 'Device'
+  }
+}
+
+device_modules {
+  key: "udon_emulator"
+  value: {
+    device_module: 'udm.device.udon'
+    device_class: 'Udon'
+  }
+}
+
+device_modules {
+  key: "valens"
+  value: {
+    device_module: 'eurtest.device.assistant_device'
+    device_class: 'Assistant'
+  }
+}
+
+device_modules {
+  key: "vento"
+  value: {
+    device_module: 'eurtest.device.assistant_device'
+    device_class: 'Assistant'
+  }
+}
+
+device_modules {
+  key: "venus"
+  value: {
+    device_module: 'udm.device.nest_iot_device'
+    device_class: 'SpencerDevice'
+  }
+}
+
+#####################
+## Test Group Tags ##
+#####################
+#  Defines common test group tags.
+
+test_group_tags: [
+  # Tests that verify OTA installs successfully and doesn't brick the device.
+  # No other tests will be run for a given build if any test in OTA_SMOKE fails.
+  "OTA_SMOKE",
+  "UNIT_TESTS",
+  "RECONNECT_TEST",
+  "GQNQ_CAMERA_ENGPROD_PRESUBMIT",
+  "GQNQ_VIRTUAL_PRESUBMIT"
+]
+
+#############################
+## Continuous Test Targets ##
+#############################
+
+disabled_branches: "1.39nf"
+disabled_branches: "1.39nfct"
+disabled_branches: "1.42rd"
+disabled_branches: "1.46rd"
+disabled_branches: "1.49qfct"
+
+test_configs_root: "lab_system/master/config/tests/"
+
+# AudioPassthroughFormats
+
+#################### Fuchsia #####################################
+#  Please keep tests ordered by binary name within this section. #
+##################################################################
+continuous_test: {
+  test_target: {
+    binary_name: "cycle_bitrates"
+    config: "cycle_bitrates/fuchsia_cycle_bitrates.cfg"
+    bot_pool : "gazoo-prod"
+    recipe: FUCHSIA_RUNNER
+    skip_default_dimensions: true
+    testbed_requirements: [
+      {bot_dimension: "product", value: "nelson"},
+      {bot_dimension: "device_type", value: "nelson"},
+      {bot_dimension: "group", value: "COMMON"}
+    ]
+  }
+  build_targets: {
+    products: ["astro"]
+    branches: [{name: "1.52f"} , {name: "master"}]
+  }
+}
+
+continuous_test: {
+  test_target: {
+    binary_name: "cycle_bitrates"
+    config: "cycle_bitrates/fuchsia_cycle_bitrates.cfg"
+    bot_pool : "gazoo-prod"
+    recipe: FUCHSIA_RUNNER
+    skip_default_dimensions: true
+    testbed_requirements: [
+      {bot_dimension: "product", value: "sherlock"},
+      {bot_dimension: "device_type", value: "sherlock"},
+      {bot_dimension: "group", value: "COMMON"}
+    ]
+  }
+  build_targets: {
+    products: ["astro"]
+    branches: [{name: "1.52f"} , {name: "master"}]
+  }
+}
+#################### End of Fuchsia configurations #################
+
+scheduled_test: {
+  test_target: {
+    config: "perception_latency/perception_latency.cfg"
+    binary_name: "perception_latency"
+    bot_pool : "gazoo-prod"
+    test_cases: ["PerceptionLatencyTestCaseMotion"]
+    test_timeout_mins: 120
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "perception_latency"}
+    ]
+  }
+  schedule: {
+    cron: "0 * * * *"  # Every 60 mins.
+    policy: LATEST_UNTESTED
+  }
+  build_targets: {
+    products: ["newman"]
+    variants: USER
+  }
+  branches: ["1.44", "1.45", "1.42", "master"]
+}
+
+scheduled_test: {
+  test_target: {
+    config: "selina/gesture_data_injection.cfg"
+    binary_name: "selina"
+    bot_pool: "gazoo-prod"
+    enabled: true
+    test_timeout_mins: 180
+    testbed_requirements: [
+    {bot_dimension: "GROUP", value: "selina_data_injection"}
+    ]
+  }
+  schedule: {
+    cron: "30 */2 * * *"  # Every 2 hour
+    policy: LATEST
+  }
+  build_targets: {
+    products: ["elaine"]
+    variants: ENG
+  }
+  branches: ["master", "1.56", "1.56.1"]
+}
+
+scheduled_test: {
+  test_target: {
+    config: "selina/sleep_data_injection.cfg"
+    binary_name: "selina"
+    bot_pool: "gazoo-prod"
+    enabled: true
+    test_timeout_mins: 780
+    testbed_requirements: [
+    {bot_dimension: "GROUP", value: "sleep_ext4"}
+    ]
+  }
+  schedule: {
+    cron: "45 */4 * * *"  # Every 4 hour
+    policy: LATEST
+  }
+  build_targets: {
+    products: ["elaine"]
+    variants: ENG
+  }
+  branches: ["master", "1.56", "1.56.1"]
+}
+
+scheduled_test: {
+  test_target: {
+    config: "sound_sensing/sound_sensing_mic_writer.cfg"
+    binary_name: "sound_sensing"
+    bot_pool: "gazoo-prod"
+    enabled: false
+    test_timeout_mins: 1380
+    testbed_requirements: [
+    {bot_dimension: "GROUP", value: "sound_sensing_mic_writer"}
+    ]
+  }
+  schedule: {
+    cron: "30 */8 * * *"  # Every 8 hour
+    policy: LATEST
+  }
+  build_targets: {
+    products: ["biggie", "elaine", "mushroom", "newman", "pineapple", "valens", "vento"]
+    variants: ENG
+  }
+  branches: ["master", "1.54"]
+}
+
+scheduled_test: {
+  test_target: {
+    config: "sound_sensing/sound_sensing_mic_writer_p1.cfg"
+    binary_name: "sound_sensing"
+    bot_pool: "gazoo-prod"
+    enabled: false
+    test_timeout_mins: 1380
+    testbed_requirements: [
+    {bot_dimension: "GROUP", value: "sound_sensing_mic_writer"}
+    ]
+  }
+  schedule: {
+    cron: "0 */12 * * *"  # Every 12th hour
+    policy: LATEST
+  }
+  build_targets: {
+    products: ["biggie", "elaine", "mushroom", "newman", "pineapple", "valens", "vento"]
+    variants: ENG
+  }
+  branches: ["master", "1.54"]
+}
+
+scheduled_test: {
+  test_target: {
+    config: "sound_sensing/sound_sensing_mic_writer_p2.cfg"
+    binary_name: "sound_sensing"
+    bot_pool: "gazoo-prod"
+    enabled: false
+    test_timeout_mins: 1380
+    testbed_requirements: [
+    {bot_dimension: "GROUP", value: "sound_sensing_mic_writer"}
+    ]
+  }
+  schedule: {
+    cron: "0 */12 * * *"  # Every 12th hour
+    policy: LATEST
+  }
+  build_targets: {
+    products: ["biggie", "elaine", "mushroom", "newman", "pineapple", "valens", "vento"]
+    variants: ENG
+  }
+  branches: ["master", "1.54"]
+}
+
+continuous_test: {
+  test_target: {
+    enabled: false
+    config: "gqnq_contract_executor/gqnq_contract_executor_unpaired.cfg"
+    binary_name: "gqnq_contract_executor"
+    bot_pool: "gazoo-prod"
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "camie"}
+    ]
+  }
+  build_targets: {
+    products: ["gq", "nq", "spencer"]
+    variants: ENG
+  }
+}
+
+continuous_test: {
+  test_target: {
+    config: "gqnq_contract_executor/gqnq_contract_executor_virtual_presubmit.cfg"
+    enabled: false
+    binary_name: "gqnq_contract_executor"
+    bot_pool: "gazoo-staging"
+    skip_default_dimensions: true
+    recipe: VIRTUAL_DEVICE_RUNNER
+    test_groups: ["GQNQ_VIRTUAL_PRESUBMIT"]
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "COMMON"},
+      {bot_dimension: "testbed_type", value: "vdm-cq"}
+    ]
+  }
+  build_targets: {
+    products: ["gq_x86_64", "nq_x86_64", "spencer_x86_64"]
+    variants: ENG
+    branches: [ { name: "master" } ]
+  }
+}
+
+continuous_test: {
+  test_target: {
+    config: "gqnq_contract_executor/gqnq_contract_executor_presubmit.cfg"
+    enabled: false
+    binary_name: "gqnq_contract_executor"
+    test_groups: ["GQNQ_CAMERA_ENGPROD_PRESUBMIT"]
+    bot_pool: "gazoo-prod"
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "camie-cq"}
+    ]
+  }
+  build_targets: {
+    products: ["gq", "nq"]
+    variants: ENG
+    branches: [ { name: "master" } ]
+  }
+}
+
+scheduled_test: {
+  test_target: {
+    config: "selina/sleep_chester.cfg"
+    binary_name: "selina"
+    bot_pool: "gazoo-prod"
+    enabled: false
+    test_timeout_mins: 780
+    testbed_requirements: [
+    {bot_dimension: "GROUP", value: "sleep_positive"}
+    ]
+  }
+  schedule: {
+    cron: "30 0,12 * * *"  # Every 12 hour
+    policy: LATEST
+  }
+  build_targets: {
+    products: ["elaine"]
+    variants: ENG
+  }
+  branches: ["1.56.2"]
+}
+
+scheduled_test: {
+  test_target: {
+    config: "selina/sleep_chester_1_1.cfg"
+    binary_name: "selina"
+    bot_pool: "gazoo-prod"
+    enabled: false
+    test_timeout_mins: 780
+    testbed_requirements: [
+    {bot_dimension: "GROUP", value: "sleep_positive"}
+    ]
+  }
+  schedule: {
+    cron: "30 6,18 * * *"  # Every 12 hour
+    policy: LATEST
+  }
+  build_targets: {
+    products: ["elaine"]
+    variants: ENG
+  }
+  branches: ["1.56.2"]
+}
+
+scheduled_test: {
+  test_target: {
+    config: "selina/sleep_chester_2_1.cfg"
+    binary_name: "selina"
+    bot_pool: "gazoo-prod"
+    enabled: false
+    test_timeout_mins: 780
+    testbed_requirements: [
+    {bot_dimension: "GROUP", value: "sleep_positive"}
+    ]
+  }
+  schedule: {
+    cron: "30 3,15 * * *"  # Every 12 hour
+    policy: LATEST
+  }
+  build_targets: {
+    products: ["elaine"]
+    variants: ENG
+  }
+  branches: ["1.56.2"]
+}
+
+######################################################
+#                                                    #
+# Libassistant configs.                              #
+#                                                    #
+######################################################
+continuous_test: {
+  test_target: {
+    binary_name: "libassistant-integration-test"
+    priority: CRITICAL
+    recipe: LIBASSISTANT_RUNNER
+    test_timeout_mins: 120  # Two hours
+    cipd_refs_key: "libassistant"
+    test_cases: ["testOnArm"]
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "COMMON"}
+    ]
+  }
+  build_targets: {
+    branch_types: MASTER
+    products: ["assistant"]
+    variants: ARM
+  }
+}
+
+continuous_test: {
+  test_target: {
+    binary_name: "libassistant-integration-test"
+    priority: CRITICAL
+    recipe: LIBASSISTANT_RUNNER
+    test_timeout_mins: 120  # Two hours
+    cipd_refs_key: "libassistant"
+    test_cases: ["testOnX64"]
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "COMMON"}
+    ]
+  }
+  build_targets: {
+    branch_types: MASTER
+    products: ["assistant"]
+    variants: X64
+  }
+}
diff --git a/catatester/configs/staging/continuous_tests.protoascii b/catatester/configs/staging/continuous_tests.protoascii
new file mode 100644
index 0000000..bbae571
--- /dev/null
+++ b/catatester/configs/staging/continuous_tests.protoascii
@@ -0,0 +1,395 @@
+# -*- mode: protobuffer; -*-
+# proto-file: catatester/proto/continuous_tests.proto
+# proto-message: ContinuousTestsConfig
+
+#############################################
+## CIPD packages associated with a repo    ##
+#############################################
+# Different repos require different recipes
+# bundled in cipd packages. If version is empty
+# then it will replaced by the reference tag
+# provided by the controller.
+cipd_refs {
+  key: "libassistant"
+  value: {
+    refs: [
+      {
+        package_name: "home_internal/libassistant_tools"
+        version: ""
+        path: "."
+      }
+    ]
+  }
+}
+cipd_refs {
+  key: "internal"
+  value: {
+    refs: [
+      {
+        package_name: "home_internal/recipes"
+        version: ""
+        path: "."
+      }
+    ]
+  }
+}
+cipd_refs {
+  key: "internal_with_opal_dart",
+  value: {
+    refs: [
+      {
+        package_name: "home_internal/recipes"
+        version: ""
+        path: "."
+      },
+      {
+        package_name: "home_internal/experimental/flutter_bridge"
+        version: "latest"
+        path: "."
+      }
+    ]
+  }
+}
+#####################
+## Device modules  ##
+#####################
+# Different devices require different classes to control them. If the device
+# product is not defined in this map the following defaults will be used:
+#   device_module = eurtest.device.device
+#   device_class = Device
+device_modules {
+  key: "assistant_arm"
+  value: {
+    device_module: 'udm.device.raspberry_pi'
+    device_class: 'RaspberryPi'
+  }
+}
+
+device_modules {
+  key: "assistant_x64"
+  value: {
+    device_module: 'udm.device.assistant_x64'
+    device_class: 'AssistantX64'
+  }
+}
+
+device_modules {
+  key: "caprica"
+  value: {
+    device_module: 'eurtest.device.device'
+    device_class: 'Device'
+  }
+}
+
+device_modules {
+  key: "pepperoni"
+  value: {
+    device_module: 'eurtest.device.chromecast_audio'
+    device_class: 'ChromecastAudio'
+  }
+}
+
+device_modules {
+  key: "mushroom"
+  value: {
+    device_module: 'eurtest.device.assistant_device'
+    device_class: 'Assistant'
+  }
+}
+
+device_modules {
+  key: "newman"
+  value: {
+    device_module: 'eurtest.device.assistant_device'
+    device_class: 'Assistant'
+  }
+}
+
+device_modules {
+  key: "pineapple"
+  value: {
+    device_module: 'eurtest.device.assistant_device'
+    device_class: 'Assistant'
+  }
+}
+
+device_modules {
+  key: "sabrina"
+  value: {
+    device_module: 'eurtest.device.android'
+    device_class: 'Android'
+  }
+}
+
+device_modules {
+  key: "vento"
+  value: {
+    device_module: 'eurtest.device.assistant_device'
+    device_class: 'Assistant'
+  }
+}
+
+# Experiment bot type with no device associated with
+device_modules {
+  key: "x86_worker"
+  value: {}
+}
+
+######################
+## Build Priorities ##
+######################
+
+branch_type_priorities: [RELEASE, MASTER]
+
+#############################
+## Continuous Test Targets ##
+#############################
+test_configs_root: "lab_system/master/config/tests/"
+
+test_group_tags: [
+  # Tests that verify OTA installs successfully and doesn't brick the device.
+  # No other tests will be run for a given build if any test in OTA_SMOKE fails.
+  "OTA_SMOKE",
+  "UNIT_TESTS"
+]
+
+disabled_branches: "1.39nf"
+disabled_branches: "1.39nfct"
+disabled_branches: "1.42rd"
+
+# Assistant end to end query latency tests.
+scheduled_test: {
+  test_target: {
+    config: "assistant_query_latency/canonical_queries_latency_dogfood.cfg"
+    binary_name: "assistant_query_latency"
+    enabled: false
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "chirp_latency"}
+    ]
+  }
+  schedule: {
+    cron: "*/30 * * * *"  # Every 30 mins
+    policy: LATEST
+  }
+  build_targets: {
+    branch_types: MASTER
+    products: ["pineapple"]
+    variants: USER
+  }
+}
+
+continuous_test: {
+  test_target: {
+    config: "ota_installable/ota_installable.cfg"
+    binary_name: "ota_installable"
+    priority: CRITICAL
+    test_groups: ["OTA_SMOKE"]
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "COMMON"}
+    ]
+    recipe: OTA_INSTALLABLE_RUNNER
+  }
+  build_targets: {
+    branch_types: MASTER
+    products: ["prince", "valens"]
+    variants: ENG
+  }
+}
+
+continuous_test: {
+  test_target: {
+    config: "crash_loop/crash_loop.cfg"
+    binary_name: "crash_loop"
+    priority: CRITICAL
+    test_groups: ["OTA_SMOKE"]
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "COMMON"}
+    ]
+    recipe: OTA_INSTALLABLE_RUNNER
+  }
+  build_targets: {
+    branch_types: MASTER
+    products: ["prince", "valens"]
+    variants: ENG
+  }
+}
+
+# This config is here just as an example of a config that uses builder_names.
+# Test is disabled.
+continuous_test: {
+  test_target: {
+    config: "cast/receiver.cfg"
+    binary_name: "cast"
+    enabled: false
+    test_cases: [
+      "CastReceiverCapabilityTestCase"
+    ]
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "COMMON"}
+    ]
+  }
+  build_targets: {
+    builder_names: ["steak-yeti-user"]
+    branch_types: MASTER
+    variants: USER
+  }
+}
+
+continuous_test: {
+  test_target: {
+    config: "reconnect_test/reconnect_test.cfg"
+    binary_name: "reconnect_test"
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "COMMON"}
+    ]
+  }
+  build_targets: {
+    branch_types: MASTER
+    products: ["newman"]
+    variants: ENG
+  }
+}
+
+continuous_test: {
+  test_target: {
+    config: "ndm_integration/ndm_integration.cfg"
+    binary_name: "ndm_integration"
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "COMMON"}
+    ]
+  }
+  build_targets: {
+    branch_types: MASTER
+    products: ["newman"]
+    variants: USER
+  }
+}
+
+continuous_test: {
+  test_target: {
+    config: "fakeoculus_reconnect_proxy/fakeoculus_reconnect_proxy.cfg"
+    binary_name: "fakeoculus_reconnect_proxy"
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "COMMON"}
+    ]
+  }
+  build_targets: {
+    branch_types: MASTER
+    products: ["newman"]
+    variants: USER
+  }
+}
+
+continuous_test: {
+  test_target: {
+    config: "cast_video/video_encode_accelerator.cfg"
+    binary_name: "cast_video"
+    test_timeout_mins: 120
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "COMMON"}
+    ]
+  }
+  build_targets: {
+    branch_types: MASTER
+    products: ["newman"]
+    variants: USER
+  }
+}
+
+continuous_test: {
+  test_target: {
+    config: "cast_video/video_decode_accelerator.cfg"
+    binary_name: "cast_video"
+    test_timeout_mins: 120
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "COMMON"}
+    ]
+  }
+  build_targets: {
+    branch_types: MASTER
+    products: ["newman"]
+    variants: USER
+  }
+}
+
+######################################################
+#                                                    #
+# Libassistant configs.                              #
+#                                                    #
+######################################################
+
+# Disable test for Gazoo libassistant experiment b/161898246.
+# continuous_test: {
+#   test_target: {
+#     binary_name: "libassistant-integration-test"
+#     priority: CRITICAL
+#     recipe: LIBASSISTANT_RUNNER
+#     test_timeout_mins: 120  # Two hours
+#     cipd_refs_key: "libassistant"
+#     test_cases: ["testOnArm"]
+#   }
+#   build_targets: {
+#     branch_types: MASTER
+#     products: ["assistant"]
+#     variants: ARM
+#   }
+# }
+
+continuous_test: {
+  test_target: {
+    binary_name: "libassistant-integration-test"
+    priority: CRITICAL
+    recipe: LIBASSISTANT_RUNNER
+    test_timeout_mins: 120  # Two hours
+    cipd_refs_key: "libassistant"
+    test_cases: ["testOnX64"]
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "COMMON"}
+    ]
+  }
+  build_targets: {
+    branch_types: MASTER
+    products: ["assistant"]
+    variants: X64
+  }
+}
+######################################################
+#                                                    #
+# Thread Unit Tests configs.                         #
+#                                                    #
+######################################################
+continuous_test: {
+  test_target: {
+    config: "thread/thread.cfg"
+    binary_name: "thread"
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "COMMON"}
+    ]
+  }
+  build_targets: {
+    branch_types: MASTER
+    products: ["newman"]
+    variants: USER
+  }
+}
+
+scheduled_test: {
+  test_target: {
+    cipd_refs_key: "internal_with_opal_dart"
+    config: "manhattan_ui_perf/opal_back_gestures.cfg"
+    binary_name: "manhattan_ui_perf"
+    test_timeout_mins: 30
+    testbed_requirements: [
+      {bot_dimension: "GROUP", value: "COMMON"}
+    ]
+  }
+  schedule: {
+    cron: "0 0 * * *"  # Every day at 00:00 hours
+    policy: LATEST
+  }
+  build_targets: {
+    branch_types: MASTER
+    products: ["newman"]
+    variants: USER
+  }
+}
diff --git a/catatester/devicemap/__init__.py b/catatester/devicemap/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/catatester/devicemap/__init__.py
diff --git a/catatester/devicemap/bot_setup.py b/catatester/devicemap/bot_setup.py
new file mode 100644
index 0000000..7b3bbf4
--- /dev/null
+++ b/catatester/devicemap/bot_setup.py
@@ -0,0 +1,277 @@
+"""A helper script to setup and start bots on a host machine.
+
+  This script does following:
+    - Parses devicemap.yaml and finds out how many bots are needed for a host.
+    - Check if bot directories exist and create bot directories as needed.
+    - Create systemd configs and bot startup script.
+    - Run systemctl start swarming_bot--<bot number> for any bots not currently
+      running
+
+NOTE: This script is being invoked from a puppet manifest and will not have
+access to rest of the test code or python virtual environment. So keep in mind
+to not use any non standard python libraries.
+
+Example usage:
+  python bot_setup.py bot_setup.py \
+    --user gtvchrome --group gtvchrome --swarm-bots-home /home/bots_home \
+    --gsutil-dir /home/gsutil_home --gsutil-boto /home/.boto \
+    --fragglerock-key /home/fragglerock_service_key.json \
+    --eurtest-binaries /home/eurtest_binaries --venv /home/bots-home/swarm_env
+"""
+
+import argparse
+import errno
+import grp
+import logging
+import os
+import pwd
+import shutil
+import socket
+import string
+import subprocess
+import sys
+import yaml
+
+# This code runs on catatester host and doesn't have whole code tree, hence
+# do relative import.
+try:
+  from catatester.devicemap import devicemap_lib
+except ImportError:
+  import devicemap_lib  # pylint: disable=relative-import
+
+
+_BOT_FILE_MODE = 0o774  # permit owner group execution
+_SYSTEMD_FILE_MODE = 0o644  # permit owner group write
+_ROOT_USER = 'root'
+# TODO(adrexler): Remove _DEVICEMAP_DIR once the arg is being passed from
+# puppet.
+_DEVICEMAP_DIR = os.path.dirname(os.path.realpath(__file__))
+_STARTUP_SCRIPT_TEMPLATE_PATH = os.path.join(
+    _DEVICEMAP_DIR, 'swarm_bot_startup.sh.template')
+_STARTUP_SCRIPTS_PATH = 'startup_scripts'
+_SWARM_BOT_PREFIX = 'swarming_bot--'
+_SWARM_ZIP_RELATIVE_PATH_TO_BOT_HOME = '1/swarming_bot.zip'
+_SYSTEMD_TEMPLATE_PATH = os.path.join(
+    _DEVICEMAP_DIR, 'swarm_bot.systemd.template')
+_DEPOT_TOOLS_DIR = os.path.join(os.path.expanduser('~'), 'depot_tools')
+
+
+class OSException(Exception):
+  """Exception class to handle OS Exceptions."""
+
+
+def _GetNumBotConfigs(hostname):
+  """Gets the number of bot configs assigned to the given hostname.
+
+  Args:
+    hostname: (String) Catatester hostame.
+
+  Returns:
+    An integer, The number of bot configs for given hostname.
+  """
+  logging.debug('Finding bots needed for host [%s]', hostname)
+  configs = devicemap_lib.Configs()
+  return configs.GetNumBotConfigs(hostname)
+
+
+def CreateStartupScript(
+    user, group, swarm_bots_home, bot_num, bot_name, gsutil_dir,
+    gsutil_boto, fragglerock_key, eurtest_binaries, venv,
+    devicemap_dir, depot_tools_dir):
+  """Create the bot startup script.
+
+  This method loads tokenized swarm_bot_startup.sh.template file, replaces the
+  tokens with given param values and writes it into bot folder.
+
+  Args:
+    user: (String) User name of the catatester host.
+    group: (String) Group name that is user is part of.
+    swarm_bots_home: (String) Root directory of all the bots.
+    bot_num: (Integer) The N-th bot to be created.
+    bot_name: (String) Bot service name.
+    gsutil_dir: (String) Path to gsutil installation in the host.
+    gsutil_boto: (String) Path to gsutil boto file in the host.
+    fragglerock_key: (String) Path to auth key file in the host.
+    eurtest_binaries: (String) Path to test binaries in the host.
+    venv: (String) Path to python virtual environment to be used.
+    devicemap_dir: (String) Path to devicemap directory on host.
+    depot_tools_dir: (String) Path to depot tools directory.
+
+  Returns:
+    The path of newly created startup script.
+  """
+  bot_dir_path = os.path.join(swarm_bots_home, str(bot_num))
+  with open(_STARTUP_SCRIPT_TEMPLATE_PATH, 'r') as bot_startup:
+    bot_startup_template = bot_startup.read()
+  startup_script_content = string.Template(bot_startup_template).substitute(
+      bot_dir=bot_dir_path,
+      devicemap_dir=devicemap_dir,
+      eurtest_binaries=eurtest_binaries,
+      fragglerock_key=fragglerock_key,
+      gsutil_dir=gsutil_dir,
+      gsutil_boto=gsutil_boto,
+      venv=venv,
+      depot_tools_dir=depot_tools_dir
+  )
+  startup_script_path = os.path.join(swarm_bots_home, _STARTUP_SCRIPTS_PATH,
+                                     '%s_startup.sh' % bot_name)
+  _CreateFile(
+      startup_script_path, startup_script_content, user, group, _BOT_FILE_MODE)
+  return startup_script_path
+
+
+def CreateSystemdServiceFile(
+    bot_service_name, startup_script_path, user, group):
+  """Create the systemd service script for a bot.
+
+  This method loads tokenized swarm_bot.systemd.template file, replaces the
+  tokens with given param values and writes it into systemd service folder.
+
+  Args:
+    bot_service_name: (String) The systemd service name to be used.
+    startup_script_path: (String) Startup script path of the bot.
+    user: (String) User name of the catatester host.
+    group: (String) Group name that is user is part of.
+  """
+  with open(_SYSTEMD_TEMPLATE_PATH, 'r') as bot_systemd:
+    bot_systemd_template = bot_systemd.read()
+  systemd_service_content = string.Template(bot_systemd_template).substitute(
+      bot_name=bot_service_name,
+      group=group,
+      user=user,
+      startup_script_path=startup_script_path)
+  systemd_service_path = '/etc/systemd/system/%s.service' % bot_service_name
+  _CreateFile(
+      systemd_service_path, systemd_service_content, _ROOT_USER, _ROOT_USER,
+      _SYSTEMD_FILE_MODE)
+
+
+def SetupBot(
+    bot_num, user, group, swarm_bots_home, swarm_bot_prefix, swarm_zip_path,
+    gsutil_dir, gsutil_boto, fragglerock_key, eurtest_binaries, venv,
+    devicemap_dir, depot_tools_dir):
+  """Setup the swarming bot.
+
+  This method :
+   1. Creates bot directory.
+   2. Copies the swarm binary to bot directory.
+   3. Creates start up scripts and systemd configs the bot.
+   4. Do a systemd config reload.
+   5. And finally start the bot service, if its not up.
+
+  Args:
+    bot_num: (Integer) The N-th bot to be created.
+    user: (String) User name of the catatester host.
+    group: (String) Group name that is user is part of.
+    swarm_bots_home: (String) Root directory of the bots.
+    swarm_bot_prefix: (String) Prefix to be used for bot service name.
+    swarm_zip_path: (String) Swarming binary path in the host.
+    gsutil_dir: (String) Path to gsutil installation in the host.
+    gsutil_boto: (String) Path to gsutil boto file in the host.
+    fragglerock_key: (String) Path to auth key file in the host.
+    eurtest_binaries: (String) Path to test binaries in the host.
+    venv: (String) Path to python virtual environment to be used.
+    devicemap_dir: (String) Path to the device map directory.
+    depot_tools_dir: (String) Path to depot tools directory.
+  """
+  logging.debug('Working on bot #%s', bot_num)
+  # Create bot directory.
+  bot_dir_path = os.path.join(swarm_bots_home, str(bot_num))
+  if not os.path.exists(bot_dir_path):
+    logging.info('Creating directory: %s', bot_dir_path)
+    os.makedirs(bot_dir_path)
+    _UpdateFilePermissions(bot_dir_path, user, group, _BOT_FILE_MODE)
+  # Copy swarm binary
+  if os.path.dirname(swarm_zip_path) != bot_dir_path:
+    shutil.copy(swarm_zip_path, bot_dir_path)
+    _UpdateFilePermissions(swarm_zip_path, user, group, _BOT_FILE_MODE)
+  # Create startup scripts.
+  bot_name = '%s%s' % (swarm_bot_prefix, bot_num)
+  startup_script_path = CreateStartupScript(
+      user, group, swarm_bots_home, bot_num, bot_name, gsutil_dir, gsutil_boto,
+      fragglerock_key, eurtest_binaries, venv, devicemap_dir, depot_tools_dir)
+  CreateSystemdServiceFile(bot_name, startup_script_path, user, group)
+  # Reload the daemon, check bot status and start service if needed.
+  if subprocess.call(['systemctl', 'daemon-reload']):
+    sys.exit('Failed to reload systemctl daemon for bot # %s' % bot_num)
+  if subprocess.call(['systemctl', 'is-active', '--quiet', bot_name]):
+    logging.debug('Bot number [%s] not running. Starting now.', bot_num)
+    if subprocess.call(['systemctl', 'start', bot_name]):
+      sys.exit('Failed to start bot # %s' % bot_num)
+
+
+def _UpdateFilePermissions(file_path, owner, group, mode):
+  """Update file with specified mode and permissions."""
+  uid = pwd.getpwnam(owner).pw_uid
+  gid = grp.getgrnam(group).gr_gid
+  os.chown(file_path, uid, gid)
+  os.chmod(file_path, mode)
+
+
+def _CreateFile(file_path, content, owner, group, mode):
+  """Create file with specified content, mode and permissions."""
+  if not os.path.exists(os.path.dirname(file_path)):
+    try:
+      os.makedirs(os.path.dirname(file_path))
+      _UpdateFilePermissions(os.path.dirname(file_path), owner, group, mode)
+    except OSError as exc:
+      if exc.errno != errno.EEXIST:
+        raise OSException('Unable to create dir %s due to %s.'
+                          % (os.path.dirname(file_path), exc))
+
+  with open(file_path, 'w') as f:
+    f.write(content)
+  _UpdateFilePermissions(file_path, owner, group, mode)
+
+
+def _ParseArgs():
+  """Parses command-line input."""
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      '--user', help='User name of the catatester host.', required=True)
+  parser.add_argument('--group', help='Group name to be used.', required=True)
+  parser.add_argument(
+      '--swarm_bots_home', help='Root directory of the bots.', required=True)
+  parser.add_argument(
+      '--gsutil_dir', help='Path to gsutil installation.', required=True)
+  parser.add_argument(
+      '--gsutil_boto', help='Path to gs util boto file.', required=True)
+  parser.add_argument(
+      '--fragglerock_key', help='Path to fragglerock auth key file.',
+      required=True)
+  parser.add_argument(
+      '--eurtest_binaries', help='Test binary path.', required=True)
+  parser.add_argument(
+      '--venv', help='Path to python to virtual environment.', required=True)
+  parser.add_argument(
+      '--devicemap_dir', help='Path to devicemap directory on the host.',
+      default=_DEVICEMAP_DIR)
+  parser.add_argument(
+      '--depot_tools_dir', help='Path to depot_tools in the host.',
+      default=_DEPOT_TOOLS_DIR)
+  args = parser.parse_args()
+  logging.debug('Executing with flags: %s', args)
+  return args
+
+
+def main():
+  flags = _ParseArgs()
+  bot_count = _GetNumBotConfigs(socket.gethostname().split('.')[0])
+  swarm_bots_home = flags.swarm_bots_home
+  swarm_zip_path = os.path.join(
+      swarm_bots_home, _SWARM_ZIP_RELATIVE_PATH_TO_BOT_HOME)
+  if not os.path.exists(swarm_zip_path):
+    raise ValueError('Swarm binary does not exists at %s' % swarm_zip_path)
+  for bot_num in range(1, bot_count +1):
+    SetupBot(
+        bot_num=bot_num, user=flags.user, group=flags.group,
+        swarm_bots_home=swarm_bots_home, swarm_bot_prefix=_SWARM_BOT_PREFIX,
+        swarm_zip_path=swarm_zip_path, gsutil_dir=flags.gsutil_dir,
+        gsutil_boto=flags.gsutil_boto, fragglerock_key=flags.fragglerock_key,
+        eurtest_binaries=flags.eurtest_binaries, venv=flags.venv,
+        devicemap_dir=flags.devicemap_dir,
+        depot_tools_dir=flags.depot_tools_dir)
+
+
+if __name__ == '__main__':
+  main()
diff --git a/catatester/devicemap/bot_setup_test.py b/catatester/devicemap/bot_setup_test.py
new file mode 100644
index 0000000..2784650
--- /dev/null
+++ b/catatester/devicemap/bot_setup_test.py
@@ -0,0 +1,111 @@
+"""Tests for the bot_setup module."""
+import os
+import shutil
+import subprocess
+import tempfile
+import unittest
+from catatester.devicemap import bot_setup
+
+try:
+  from unittest import mock
+except ImportError:
+  import mock
+
+_STARTUP_SCRIPTS_PATH = 'startup_scripts'
+
+
+class BotSetupTest(unittest.TestCase):
+  """Tests for the methods in bot_setup module."""
+
+  def setUp(self):
+    self._swarm_bots_home = tempfile.mkdtemp()
+    self.addCleanup(self.cleanUp)
+    self.addCleanup(mock.patch.stopall)
+    self._mock_subprocess_call = mock.patch(
+        'subprocess.call', autospec=True, spec_set=True).start()
+    self._mock_copy_file = mock.patch(
+        'shutil.copy', autospec=True, spec_set=True).start()
+    self._mock_create_file = mock.patch(
+        'catatester.devicemap.bot_setup._CreateFile',
+        autospec=True, spec_set=True).start()
+    self._mock_update_file_mode = mock.patch(
+        'catatester.devicemap.bot_setup._UpdateFilePermissions',
+        autospec=True, spec_set=True).start()
+
+  def cleanUp(self):
+    shutil.rmtree(self._swarm_bots_home)
+
+  def testSetupBotsCreatesNewDirCopiesFileAndStartsService(self):
+    """Validates setup_bot method can successfully start bot service."""
+    self._mock_subprocess_call.side_effect = [0, 1, 0]
+    bot_num = 2
+    bot_setup.SetupBot(
+        bot_num=bot_num,
+        user='mock_user',
+        group='mock_group',
+        swarm_bots_home=self._swarm_bots_home,
+        swarm_bot_prefix='mock_swarm_bot_prefix',
+        swarm_zip_path='mock_swarm_zip_path',
+        gsutil_dir='mock_gsutil_dir',
+        gsutil_boto='mock_gsutil_boto',
+        fragglerock_key='mock_fragglerock_key',
+        eurtest_binaries='mock_eurtest_binaries',
+        venv='mock_venv',
+        devicemap_dir='mock_devicemap_dir',
+        depot_tools_dir='mock_depot_tools'
+    )
+    self.assertEqual(
+        1, len(os.listdir(self._swarm_bots_home)), 'Bot directory not created.')
+    self.assertTrue(
+        os.path.isdir(os.path.join(self._swarm_bots_home, str(bot_num))),
+        'Bot directory not created.')
+    expected_systemd_file_path = (
+        '/etc/systemd/system/mock_swarm_bot_prefix%s.service' %  bot_num)
+    expected_sh_path = (
+        os.path.join(
+            self._swarm_bots_home, _STARTUP_SCRIPTS_PATH,
+            'mock_swarm_bot_prefix%s_startup.sh' % bot_num))
+    self._mock_create_file.assert_has_calls([
+        mock.call(
+            file_path=expected_sh_path, content=mock.ANY,
+            owner='mock_user', group='mock_group', mode=508),
+        mock.call(
+            file_path=expected_systemd_file_path, content=mock.ANY,
+            owner='root', group='root', mode=420)
+    ])
+    self._mock_subprocess_call.assert_has_calls([
+        mock.call(['systemctl', 'daemon-reload']),
+        mock.call([
+            'systemctl', 'is-active', '--quiet',
+            'mock_swarm_bot_prefix%s' % bot_num
+        ]),
+        mock.call([
+            'systemctl', 'start', 'mock_swarm_bot_prefix%s' % bot_num])
+    ])
+    self._mock_update_file_mode.assert_has_calls([
+        mock.call(mock.ANY, 'mock_user', 'mock_group', 508),
+        mock.call('mock_swarm_zip_path', 'mock_user', 'mock_group', 508)
+    ])
+
+  def testBotServiceNotRestartedIfItsRunning(self):
+    """Validates setup_bot doesn't restart an already running bot service."""
+    self._mock_subprocess_call.side_effect = [0, 0]
+    bot_setup.SetupBot(
+        bot_num=1,
+        user='mock_user',
+        group='mock_group',
+        swarm_bots_home=self._swarm_bots_home,
+        swarm_bot_prefix='mock_swarm_bot_prefix',
+        swarm_zip_path='mock_swarm_zip_path',
+        gsutil_dir='mock_gsutil_dir',
+        gsutil_boto='mock_gsutil_boto',
+        fragglerock_key='mock_fragglerock_key',
+        eurtest_binaries='mock_eurtest_binaries',
+        venv='mock_venv',
+        devicemap_dir='mock_devicemap_dir',
+        depot_tools_dir='mock_depot_tools')
+    self._mock_subprocess_call.assert_called_with([
+        'systemctl', 'is-active', '--quiet', 'mock_swarm_bot_prefix1'])
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/catatester/devicemap/cipd.yaml b/catatester/devicemap/cipd.yaml
new file mode 100644
index 0000000..c5d9af6
--- /dev/null
+++ b/catatester/devicemap/cipd.yaml
@@ -0,0 +1,10 @@
+package: home_internal/devicemap
+description: Device Configs for Swarming Bots
+root: ./
+
+data:
+
+  - dir: ./
+    exclude:
+      - .*\.pyc
+      - .*\/.*  # no subdirectories needed
diff --git a/catatester/devicemap/devicemap.py b/catatester/devicemap/devicemap.py
new file mode 100644
index 0000000..54e454b
--- /dev/null
+++ b/catatester/devicemap/devicemap.py
@@ -0,0 +1,66 @@
+"""CLI to query bot dimensions for the device(s) of a given bot.
+
+Example usage:
+  python devicemap.py home-swarming-bot-host-3--7
+  (writes bot dimensions & state as python dict to stdout)
+"""
+
+import argparse
+import json
+import logging
+import os
+import re
+import yaml
+
+# This code runs on catatester host and doesn't have whole code tree, hence
+# do relative import.
+try:
+  from catatester.devicemap import devicemap_lib
+except ImportError:
+  import devicemap_lib # pylint: disable=relative-import
+
+
+_BOTNAME_PATTERN = r'(.*)--(\d*)'
+
+
+def _ParseArgs():
+  """Parses command-line input."""
+  parser = argparse.ArgumentParser()
+
+  parser.add_argument(
+    'botname',
+    help='Name of the bot, in the format <hostname>--<bot #>',
+  )
+
+  parser.add_argument(
+    '--output_file',
+    help='Absolute path to the output file to write dimensions (as json).'
+  )
+
+  # Argument Validations
+  args = parser.parse_args()
+  match = re.match(_BOTNAME_PATTERN, args.botname)
+  if not match:
+    parser.error('invalid botname')
+  if not int(match.group(2)) > 0:
+    parser.error('bot # must be > 0')
+  logging.info('Running with: %s', args)
+  return args
+
+
+def main():
+  flags = _ParseArgs()
+  match = re.match(_BOTNAME_PATTERN, flags.botname)
+  hostname = match.group(1)
+  bot_num = int(match.group(2))
+
+  configs = devicemap_lib.Configs()
+  bot_config = configs.GetBotConfig(hostname, bot_num)
+  print(bot_config)
+  if flags.output_file:
+    with open(flags.output_file, 'w') as f:
+      f.write(json.dumps(bot_config.as_dict))
+
+
+if __name__ == '__main__':
+  main()
diff --git a/catatester/devicemap/devicemap.yaml b/catatester/devicemap/devicemap.yaml
new file mode 100644
index 0000000..f4fc61f
--- /dev/null
+++ b/catatester/devicemap/devicemap.yaml
@@ -0,0 +1,271 @@
+#i Devicemap configuration file. Defines devices and maps them to a bot host.
+
+# Defines which dimensions are allowed for each type of config.
+# This ensures consistency between configs and ensure no spelling mistakes.
+allowed_dimensions:
+
+# organized by config type: Testbed, Device (i.e. salami, biggie, etc)
+  Testbed:
+    - device_ip
+    - device_type
+    - group
+    - location
+  Device:
+    - device_ip
+    - device_type
+    - group
+    - nic
+    - serial
+    - use_tunnel     # String('true', 'false'). Default 'false'
+    - factory_reset  # String('true', 'false'). Default 'true'
+    - location
+
+# Testbed & device configs, organized by hostname
+hosts:
+  home-swarming-bot-host-1:
+    host_ip: 100.115.4.128
+    hosted_devices:
+      - assistant_x64: {ip: assistant_x64_ip_2}
+      - assistant_x64: {ip: assistant_x64_ip_3}
+    location: us-mtv-946-1e3b
+
+  home-swarming-bot-host-23:
+    host_ip: 100.127.32.3
+    hosted_devices:
+      - assistant_x64: {ip: assistant_x64_ip_4}
+      - assistant_x64: {ip: assistant_x64_ip_5}
+    location: us-mtv-946-1e3b
+
+  home-swarming-bot-host-30:
+    host_ip: 100.127.36.4
+    hosted_devices:
+      - assistant_arm: {ip: 100.127.36.181}
+      - assistant_arm: {ip: 100.127.36.182}
+    location: us-mtv-1210-1f9
+
+  home-swarming-bot-host-33:
+    host_ip: 100.127.37.3
+    hosted_devices:
+      - soba_emulator:
+            ip: 'gcloud_ip_3'
+            extra_state:
+                gce_image: 'androidthings-latest'
+                gce_instance_name: 'androidthings-emulator-003'
+                gce_instance_project: 'fragglerock'
+                gce_instance_zone: 'us-central1-c'
+      - soba_emulator:
+            ip: 'gcloud_ip_4'
+            extra_state:
+                gce_image: 'androidthings-latest'
+                gce_instance_name: 'androidthings-emulator-004'
+                gce_instance_project: 'fragglerock'
+                gce_instance_zone: 'us-central1-c'
+      - soba_emulator:
+            ip: 'gcloud_ip_5'
+            extra_state:
+                gce_image: 'androidthings-latest'
+                gce_instance_name: 'androidthings-emulator-005'
+                gce_instance_project: 'fragglerock'
+                gce_instance_zone: 'us-central1-c'
+      - soba_emulator:
+            ip: 'gcloud_ip_6'
+            extra_state:
+                gce_image: 'androidthings-latest'
+                gce_instance_name: 'androidthings-emulator-006'
+                gce_instance_project: 'fragglerock'
+                gce_instance_zone: 'us-central1-c'
+      - udon_emulator:
+            ip: 'gcloud_ip_7'
+            extra_state:
+                gce_image: 'androidthings-latest'
+                gce_instance_name: 'androidthings-emulator-007'
+                gce_instance_project: 'fragglerock'
+                gce_instance_zone: 'us-central1-c'
+      - udon_emulator:
+            ip: 'gcloud_ip_8'
+            extra_state:
+                gce_image: 'androidthings-latest'
+                gce_instance_name: 'androidthings-emulator-008'
+                gce_instance_project: 'fragglerock'
+                gce_instance_zone: 'us-central1-c'
+      - udon_emulator:
+            ip: 'gcloud_ip_9'
+            extra_state:
+                gce_image: 'androidthings-latest'
+                gce_instance_name: 'androidthings-emulator-009'
+                gce_instance_project: 'fragglerock'
+                gce_instance_zone: 'us-central1-c'
+      - udon_emulator:
+            ip: 'gcloud_ip_10'
+            extra_state:
+                gce_image: 'androidthings-latest'
+                gce_instance_name: 'androidthings-emulator-010'
+                gce_instance_project: 'fragglerock'
+                gce_instance_zone: 'us-central1-c'
+      - Testbed:
+          dimensions:
+            device_type: 'newman'
+            device_ip: ['100.127.37.28', '100.127.37.29']
+            group: ['duo_multi_device']
+          extra_state:
+            devices: ['100.127.37.28', '100.127.37.29']
+          factory_reset: 'false'
+    location: us-mtv-1210-1f9
+
+  home-swarming-bot-host-34:
+    host_ip: 100.127.37.65
+    hosted_devices:
+      - assistant_x64: {ip: assistant_x64_ip_6}
+      - assistant_x64: {ip: assistant_x64_ip_7}
+      - assistant_x64: {ip: assistant_x64_ip_8}
+      - assistant_x64: {ip: assistant_x64_ip_9}
+      - Testbed:
+          dimensions:
+            device_type: 'valens'
+            device_ip: ['100.127.37.96', '100.127.37.97']
+            group: ['duo_multi_device']
+          extra_state:
+            devices: ['100.127.37.96', '100.127.37.97']
+          factory_reset: 'false'
+      - Testbed:
+          dimensions:
+            device_type: 'newman'
+            device_ip: ['100.127.37.56', '100.127.37.57']
+            group: ['duo_enabled_multi_device_test']
+          extra_state:
+            devices: ['100.127.37.56', '100.127.37.57']
+          factory_reset: 'false'
+    location: us-mtv-1210-1f9
+
+  home-swarming-comm-host-11:
+    host_ip: 100.109.25.51
+    hosted_devices:
+      - pineapple:
+          ip: 100.115.57.5
+          group: ['chirp_call_quality']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+      - mushroom:
+          ip: 100.115.57.6
+          group: ['chirp_call_quality']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+      - biggie:
+          ip: 100.115.57.7
+          group: ['chirp_call_quality']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+      - pineapple:
+          ip: 100.127.18.115
+          extra_state:
+            email: 'cast.test.dev24@gmail.com'
+            id: 523948437256
+            gaia_id: 523948437256
+          group: ['chirp_calls']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+      - pineapple:
+          ip: 100.127.18.116
+          extra_state:
+            email: 'cast.test.dev25@gmail.com'
+            id: 946528234552
+            gaia_id: 946528234552
+          group: ['chirp_calls']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+      - pineapple:
+          ip: 100.127.18.124
+          extra_state:
+            email: 'cast.test.dev41@gmail.com'
+            id: 725720652722
+            gaia_id: 725720652722
+          group: ['chirp_calls']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+      - pineapple:
+          ip: 100.127.18.132
+          extra_state:
+            email: 'cast.test.dev52@gmail.com'
+            id: 981210082107
+            gaia_id: 981210082107
+          group: ['chirp_calls']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+      - pineapple:
+          ip: 100.127.18.133
+          extra_state:
+            email: 'cast.test.dev53@gmail.com'
+            id: 725865104723
+            gaia_id: 725865104723
+          group: ['chirp_calls']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+      - mushroom:
+          ip: 100.127.19.26
+          extra_state:
+            email: 'cast.test.dev128@gmail.com'
+            id: 751760269505
+            gaia_id: 751760269505
+          group: ['chirp_calls']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+      - mushroom:
+          ip: 100.127.19.27
+          extra_state:
+            email: 'cast.test.dev129@gmail.com'
+            id: 679953433622
+            gaia_id: 679953433622
+          group: ['chirp_calls']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+      - mushroom:
+          ip: 100.127.19.28
+          extra_state:
+            email: 'cast.test.dev130@gmail.com'
+            id: 441194473759
+            gaia_id: 441194473759
+          group: ['chirp_calls']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+      - mushroom:
+          ip: 100.127.19.29
+          extra_state:
+            email: 'cast.test.dev131@gmail.com'
+            id: 773516650003
+            gaia_id: 773516650003
+          group: ['chirp_calls']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+      - mushroom:
+          ip: 100.127.19.30
+          extra_state:
+            email: 'cast.test.dev132@gmail.com'
+            id: 275275875917
+            gaia_id: 275275875917
+          group: ['chirp_calls']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+      - biggie:
+          ip: 100.127.19.101
+          extra_state:
+            email: 'cast.test.dev100@gmail.com'
+            id: 198087117950
+            gaia_id: 198087117950
+          group: ['chirp_calls']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+    location: corp-ganeti
+
+  home-swarming-corp-host-3:
+    host_ip: 100.109.25.61
+    hosted_devices:
+      - pineapple:
+          ip: 100.127.18.146
+          extra_state:
+            email: 'cast.test.dev40@gmail.com'
+            id: 1039204980935
+            gaia_id: 1039204980935
+          group: ['signed_in']
+          use_tunnel: 'true'
+          factory_reset: 'false'
+    location: corp-ganeti
diff --git a/catatester/devicemap/devicemap_config_test.py b/catatester/devicemap/devicemap_config_test.py
new file mode 100644
index 0000000..92fea5f
--- /dev/null
+++ b/catatester/devicemap/devicemap_config_test.py
@@ -0,0 +1,89 @@
+"""Tests for devicemap.yaml config file."""
+
+import unittest
+
+from catatester.devicemap import devicemap_lib
+
+
+class DeviceMapConfigTest(unittest.TestCase):
+  """Tests for devicemap config file."""
+
+  def assertBotConfigMatchesSwarmingSchema(self, bot_config):
+    self.assertIn('dimensions', bot_config)
+    self.assertIn('state', bot_config)
+    for value in bot_config['dimensions'].values():
+      self.assertTrue(hasattr(value, '__iter__'))
+
+  def assertDimensionsAllowed(self, config, configs):
+    """Checks that all the given dimensions are in allowed_dimensions."""
+    allowed_dimensions = configs.GetAllowedDimensions(config.type)
+    allowed_dimensions.add('extra_state')
+    dimensions = set(config.dimensions)
+    if not dimensions.issubset(allowed_dimensions):
+      raise devicemap_lib.DeviceMapError(
+          'Dimensions not currently allowed: %s' % ', '.join(
+              dimensions - allowed_dimensions))
+
+  def testAllowedDimensionsAreDefined(self):
+    configs = devicemap_lib.Configs()
+    self.assertTrue(configs.GetAllowedDimensions('Testbed'))
+    self.assertTrue(configs.GetAllowedDimensions('Device'))
+
+  def testEveryConfigIsValid(self):
+    configs = devicemap_lib.Configs()
+    for host in configs.hosts:
+      for bot_num in range(1, configs.GetNumBotConfigs(host) + 1):
+        try:
+          config = configs.GetBotConfig(host, bot_num)
+          self.assertIn('dimensions', config.as_dict)
+          self.assertDimensionsAllowed(config, configs)
+        except devicemap_lib.DeviceMapError as e:
+          raise AssertionError('config failure for %s--%s:\n%s\n%s' % (
+              host, bot_num, config, e))
+
+  def testNoDuplicateDevices(self):
+    configs = devicemap_lib.Configs()
+    device_ips = set()
+    device_serials = set()
+    for host in configs.hosts:
+      for bot_num in range(1, configs.GetNumBotConfigs(host) + 1):
+        config = configs.GetBotConfig(host, bot_num)
+        if config.type == 'Device':
+          dimensions = config.dimensions
+          if 'device_ip' in dimensions:
+            ip = dimensions['device_ip']
+            self.assertNotIn(ip, device_ips, 'duplicate ip found: %s' % ip)
+            device_ips.add(ip)
+          if 'serial' in dimensions:
+            serial = dimensions['serial']
+            device_serials.add(serial)
+            self.assertNotIn(ip, device_serials,
+                             'duplicate serial found: %s' % serial)
+
+  def testMultizoneHostConfig(self):
+    """Tests the configuration of multizone hosts."""
+    configs = devicemap_lib.Configs()
+    for host in configs.hosts:
+      if 'multizone' not in host:
+        continue
+      for bot_num in range(1, configs.GetNumBotConfigs(host) + 1):
+        config = configs.GetBotConfig(host, bot_num)
+        self.assertIn('audio_card_id', config.state,
+                      'No "audio_card_id" in %r:%r.' % (host, bot_num))
+        self.assertRegexpMatches(
+            config.state['audio_card_id'], r'hw:\d,\d',
+            '"audio_card_id" in %r:%r is invalid.' % (host, bot_num))
+        self.assertIn('audio_card_ports', config.state,
+                      'No "audio_card_ports" in %r:%r.' % (host, bot_num))
+        self.assertIsInstance(
+            config.state['audio_card_ports'], list,
+            '"audio_card_ports" in %r:%r is not a list.' % (host, bot_num))
+        self.assertIn('devices', config.state,
+                      'No "devices" in %r:%r.' % (host, bot_num))
+        self.assertIsInstance(
+            config.state['devices'], list,
+            '"devices" in %r:%r is not a list.' % (host, bot_num))
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/catatester/devicemap/devicemap_e2e_test.py b/catatester/devicemap/devicemap_e2e_test.py
new file mode 100644
index 0000000..2f10d02
--- /dev/null
+++ b/catatester/devicemap/devicemap_e2e_test.py
@@ -0,0 +1,99 @@
+"""E2E Tests for the devicemap.py script."""
+
+import ast
+import logging
+import os
+import subprocess
+import sys
+import unittest
+
+from catatester.devicemap import devicemap_lib
+import yaml
+
+
+_DEVICEMAP_DIR = os.path.dirname(os.path.abspath(__file__))
+_DEVICEMAP_SCRIPT = os.path.join(_DEVICEMAP_DIR, 'devicemap.py')
+# Maximum allowed bots per host
+_MAX_BOTS = 60
+_YAML_FILE = os.path.join(_DEVICEMAP_DIR, 'devicemap.yaml')
+
+
+class DeviceMapE2ETest(unittest.TestCase):
+  """E2E Tests for the devicemap.py script."""
+
+  def setUp(self):
+    super(DeviceMapE2ETest, self).setUp()
+    self.configs = None
+    with open(_YAML_FILE, 'r') as f:
+      self.configs = devicemap_lib.Configs(yaml.safe_load(f))
+
+  def testGetBotConfig_fromStdout(self):
+    p = subprocess.Popen(
+        [sys.executable, _DEVICEMAP_SCRIPT, 'home-swarming-bot-host-1--1'],
+        env={'PYTHONPATH': ':'.join(sys.path)[1:]},
+        stdin=subprocess.PIPE,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE)
+    output, err = p.communicate()
+    self.assertFalse(p.returncode, output or err)
+    botconfig = ast.literal_eval(output.decode())
+    self.assertTrue(botconfig)
+    self.assertIsInstance(botconfig, dict)
+
+  def testHost_maximumBots(self):
+    for host in self.configs.GetHosts():
+      current_bots = self.configs.GetNumBotConfigs(host)
+      message = '%s has more than %s bots [%s]' % (
+          host, _MAX_BOTS, current_bots)
+      self.assertLessEqual(current_bots, _MAX_BOTS, message)
+
+  def testNoDuplicateHostIp(self):
+    self.assertEqual(len(self.configs.GetHostIps()),
+                     len(set(self.configs.GetHostIps())))
+
+  def testNoDuplicateGaiaAccounts(self):
+    # We can use the same set for both the IDs and the Addresses
+    unique_gaia_accounts = set()
+    non_unique_gaia_accounts = {}
+    for host in self.configs.hosts:
+      for bot in self.configs.hosts[host]['hosted_devices']:
+        for surface in bot:
+          device = bot[surface]
+          if 'factory_reset' not in device:
+            continue
+          try:
+            extra_state = device['extra_state']
+            email = extra_state.get('email')
+            gaia_id = extra_state.get('gaia_id')
+            # Need to have a pair of both or none of them
+            self.assertEqual(
+                bool(email), bool(gaia_id),
+                msg='email {} and gaia_id {}'.format(email, gaia_id))
+            if not email and not gaia_id:
+              continue
+            if extra_state.get('allow_duplicate', False):
+              if email not in non_unique_gaia_accounts.keys():
+                existing_gaia_ids = non_unique_gaia_accounts.values()
+                dup_gaia_id_msg = 'gaia_id {} is found in use already'
+                self.assertNotIn(gaia_id, existing_gaia_ids,
+                                 msg=dup_gaia_id_msg.format(gaia_id))
+                non_unique_gaia_accounts[email] = gaia_id
+              else:
+                err_msg = 'gaia_id {} is inconsistent with this email {}'
+                self.assertEqual(gaia_id, non_unique_gaia_accounts[email],
+                                 msg=err_msg.format(gaia_id, email))
+            else:
+              # Email/ID cannot be re-used when allow_duplicate is set to False
+              error_msg = 'Duplicate {} is found: {}'
+              self.assertNotIn(email, unique_gaia_accounts,
+                               msg=error_msg.format('email', email))
+              self.assertNotIn(gaia_id, unique_gaia_accounts,
+                               msg=error_msg.format('gaia_id', gaia_id))
+              unique_gaia_accounts.add(email)
+              unique_gaia_accounts.add(gaia_id)
+          except KeyError:
+            logging.warning('Need account info for %s', str(device))
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/catatester/devicemap/devicemap_lib.py b/catatester/devicemap/devicemap_lib.py
new file mode 100644
index 0000000..58757e4
--- /dev/null
+++ b/catatester/devicemap/devicemap_lib.py
@@ -0,0 +1,160 @@
+"""Utility methods to retrieve devicemap.yaml data."""
+
+import json
+import logging
+import os
+import yaml
+
+
+class Error(Exception):
+  pass
+
+
+class DeviceMapError(Error):
+  pass
+
+
+class Configs(object):
+  """Represents all of the configs in the devicemap."""
+  _DEVICE_NODE_NAME = 'hosted_devices'
+  _HOST_IP = 'host_ip'
+
+  def __init__(self, configs_dict=None):
+    self._configs_dict = configs_dict or _ReadDeviceMap()
+    self.allowed_dimensions = self._configs_dict.get('allowed_dimensions')
+    self.hosts = self._configs_dict.get('hosts')
+
+  def GetAllowedDimensions(self, config_type):
+    """Gets the set() of allowed dimensions for the given type of config."""
+    if config_type not in self.allowed_dimensions:
+      raise DeviceMapError('config type not defined: %s' % config_type)
+    return set(self.allowed_dimensions[config_type])
+
+  def GetBotConfig(self, hostname, bot_num):
+    """Gets the bot config for the given hostname & bot_num (1-indexed)."""
+    host = self._GetHostConfig(hostname)
+    hosted_devices = host[self._DEVICE_NODE_NAME]
+    device_index = bot_num - 1
+    if device_index >= len(hosted_devices):
+      raise DeviceMapError('Bot #%d is not defined under host %s' % (
+        bot_num, hostname))
+    config_dict = hosted_devices[device_index]
+    host_dimensions = self._GetHostDimensions(host)
+    return BotConfig(config_dict, host_dimensions)
+
+  def GetNumBotConfigs(self, hostname):
+    """Gets the # of bot configs assigned to the given hostname."""
+    return len(self._GetHostedDevices(hostname))
+
+  def _GetHostConfig(self, hostname):
+    """Returns the complete host config dict for a given hostname."""
+    if hostname not in self.hosts:
+      raise DeviceMapError('host %s is not defined in devicemap' % hostname)
+    return self.hosts[hostname]
+
+  def _GetHostDimensions(self, host_config):
+    """Returns a dict of bot dimensions, populated from the host.
+
+    These dimensions can be overridden at the individual device level.
+    """
+    return {
+      'location': host_config.get('location', 'UNSPECIFIED')
+    }
+
+  def _GetHostedDevices(self, hostname):
+    return self._GetHostConfig(hostname)[self._DEVICE_NODE_NAME]
+
+  def GetHosts(self):
+    return self.hosts
+
+  def GetHostIps(self):
+    # Implicitly making host_ip a mandatory field here
+    return [self.hosts[h][self._HOST_IP] for h in self.hosts.keys()]
+
+
+class BotConfig(object):
+  """Represents a single bot config for a Testbed or Device."""
+
+  def __init__(self, bot_config_dict, host_dimensions=None):
+    """Initialize bot config object.
+
+    Args:
+      bot_config_dict: Dict of bot info for a single device from devicemap.
+      host_dimensions: Bot dimensions populated from the host of the device.
+    """
+    self.type = self._ParseConfigType(bot_config_dict)
+    self._raw_type = list(bot_config_dict.keys())[0]
+    self._data = bot_config_dict[self._raw_type]
+    self._host_dimensions = host_dimensions or {}
+    if self.type == 'Testbed':
+      self.dimensions, self.state = self._ParseTestbedConfig()
+    elif self.type == 'Device':
+      self.dimensions, self.state = self._ParseDeviceConfig()
+    else:
+      raise NotImplementedError('devicemap type "%s" not defined' % self.type)
+
+  def __repr__(self):
+    return str(self.as_dict)
+
+  @property
+  def as_dict(self):
+    # Transform dimensions & state to conform to swarming's schema for bots
+    for dimension, value in self.dimensions.items():
+      if not value:
+        raise DeviceMapError('Dimensions must not be empty: %s' % dimension)
+      # Dimension values must be a list/tuple
+      if not hasattr(value, '__iter__'):
+        self.dimensions[dimension] = [value]
+    return {
+      'dimensions': self.dimensions,
+      'state': self.state,
+    }
+
+  def _ParseConfigType(self, config_dict):
+    if not len(list(config_dict.keys())) == 1:
+      raise DeviceMapError(
+        ('Configuration invalid -- expected single entry for a "Testbed" '
+         'or a single device. Got: %s') % list(config_dict.keys()))
+    config_type = list(config_dict.keys())[0]
+    # Any configs that are named all in lowercase are assumed to be a device.
+    # (e.g. lexx, luther, biggie, joplin, etc). Any configs with a leading
+    # uppercase are a specified config type (e.g. Testbed).
+    if config_type.islower():
+      config_type = 'Device'
+    return config_type
+
+  def _ParseTestbedConfig(self):
+    """Get the bot config dict() for the given hostname & bot_num."""
+    dimensions = self._host_dimensions
+    dimensions.update(self._data.get('dimensions'))
+    state = self._data.get('extra_state')
+    return dimensions, state
+
+  def _ParseDeviceConfig(self):
+    """Get the bot config dict() for the given hostname & bot_num."""
+    state = {}
+    dimensions = self._host_dimensions
+    dimensions.update(self._data)
+    dimensions['device_type'] = self._raw_type
+    dimensions['group'] = dimensions.get('group') or 'COMMON'
+    dimensions['nic'] = dimensions.get('nic') or 'ethernet'
+    dimensions['use_tunnel'] = dimensions.get('use_tunnel') or 'false'
+    # FDR device between tests unless in 'signed_in' group.
+    dimensions['factory_reset'] = (
+        dimensions.get('factory_reset') or
+        str('signed_in' not in dimensions['group']).lower())
+    # Rename 'ip' to 'device_ip' for convenience (keeps config file concise)
+    if 'ip' in dimensions:
+      dimensions['device_ip'] = dimensions['ip']
+      del dimensions['ip']
+    if 'extra_state' in dimensions:
+      state = dimensions.get('extra_state', {})
+      del dimensions['extra_state']
+    return dimensions, state
+
+
+def _ReadDeviceMap():
+  """Reads all configs from devicemap.yaml config file as a dict()."""
+  pwd = os.path.dirname(os.path.realpath(__file__))
+  with open(os.path.join(pwd, 'devicemap.yaml'), 'r') as f:
+    return yaml.safe_load(f)
diff --git a/catatester/devicemap/devicemap_lib_test.py b/catatester/devicemap/devicemap_lib_test.py
new file mode 100644
index 0000000..3e6a1fa
--- /dev/null
+++ b/catatester/devicemap/devicemap_lib_test.py
@@ -0,0 +1,230 @@
+"""Tests for devicemap_lib.py module."""
+
+import unittest
+import yaml
+
+from catatester.devicemap import devicemap_lib
+
+_SAMPLE_YAML = """
+allowed_dimensions:
+  Device:
+    - ip
+    - device_ip
+hosts:
+  host-1:
+    host_ip: 1.2.3.4
+    location: 'US-MTV-B47'
+    hosted_devices:
+      - device: {ip: 192.168.0.1}
+      - device: {ip: 192.168.0.2}
+      - device:
+          ip: 192.168.0.3
+          location: 'NOT US-MTV-B47'
+"""
+
+
+class DeviceMapLibTest(unittest.TestCase):
+  """Tests for devicemap_lib.py module."""
+
+  def setUp(self):
+    self.sample_configs = devicemap_lib.Configs(yaml.safe_load(_SAMPLE_YAML))
+
+  def assertBotConfigMatchesSwarmingSchema(self, bot_config):
+    config_dict = bot_config.as_dict
+    self.assertTrue('dimensions' in config_dict)
+    self.assertTrue('state' in config_dict)
+    for value in config_dict['dimensions'].values():
+      self.assertTrue(hasattr(value, '__iter__'))
+
+  def testReadDeviceMap(self):
+    self.assertTrue(
+      devicemap_lib._ReadDeviceMap())
+
+  def testGetNumBotConfigs(self):
+    self.assertEqual(3, self.sample_configs.GetNumBotConfigs('host-1'))
+    with self.assertRaises(devicemap_lib.DeviceMapError):
+      self.sample_configs.GetNumBotConfigs('undefined-host')
+
+  def testGetAllowedDimensions(self):
+    expected = set(['ip', 'device_ip'])
+    actual = self.sample_configs.GetAllowedDimensions('Device')
+    self.assertEqual(expected, actual)
+    with self.assertRaises(devicemap_lib.DeviceMapError):
+      self.sample_configs.GetAllowedDimensions('undefined-type')
+
+  def testGetBotConfig_canGetDevice(self):
+    bot_config = self.sample_configs.GetBotConfig('host-1', 1)
+    self.assertTrue(bot_config)
+    self.assertBotConfigMatchesSwarmingSchema(bot_config)
+    self.assertIn('location', bot_config.dimensions)
+    self.assertEqual('US-MTV-B47', bot_config.dimensions['location'])
+    # Also verify works with a slightly different format (newline instead of {})
+    bot_config = self.sample_configs.GetBotConfig('host-1', 3)
+    self.assertTrue(bot_config)
+    self.assertBotConfigMatchesSwarmingSchema(bot_config)
+    self.assertTrue('device_type' in bot_config.dimensions)
+    self.assertEqual('device', bot_config.dimensions['device_type'])
+    self.assertTrue('device_ip' in bot_config.dimensions)
+    self.assertEqual('192.168.0.3', bot_config.dimensions['device_ip'])
+    self.assertIn('location', bot_config.dimensions)
+    self.assertEqual('NOT US-MTV-B47', bot_config.dimensions['location'])
+
+  def testGetBotConfig_canGetDeviceWithNewDimension(self):
+    sample_yaml = """
+hosts:
+  host-2:
+    host_ip: 2.2.2.2
+    hosted_devices:
+      - device: {ip: 192.168.0.4, new_dimension: hello}"""
+    configs = devicemap_lib.Configs(yaml.safe_load(sample_yaml))
+    bot_config = configs.GetBotConfig('host-2', 1)
+    self.assertTrue(bot_config)
+    self.assertBotConfigMatchesSwarmingSchema(bot_config)
+    expected = ('new_dimension', 'hello')
+    self.assertTrue(expected in bot_config.dimensions.items())
+
+  def testGetBotConfig_canGetDeviceWithDimensionContainingAList(self):
+    sample_yaml = """
+hosts:
+  host-2:
+    host_ip: 2.2.2.2
+    hosted_devices:
+      - device: {ip: 192.168.0.4, new_dimension: ['hello']}"""
+    configs = devicemap_lib.Configs(yaml.safe_load(sample_yaml))
+    bot_config = configs.GetBotConfig('host-2', 1)
+    self.assertTrue(bot_config)
+    self.assertBotConfigMatchesSwarmingSchema(bot_config)
+    expected = ('new_dimension', ['hello'])
+    self.assertTrue(expected in bot_config.dimensions.items())
+
+  def testGetBotConfig_canGetDeviceWithExtraState(self):
+    sample_yaml = """
+hosts:
+  host-2:
+    host_ip: 2.2.2.2
+    hosted_devices:
+      - device: {ip: 192.168.0.5, extra_state: {eth_switch: 100.107.119.209}}
+      - device:
+          ip: 192.168.0.6
+          extra_state:
+            eth_switch: 100.107.119.210
+            example: 'something new'"""
+    configs = devicemap_lib.Configs(yaml.safe_load(sample_yaml))
+    bot_config = configs.GetBotConfig('host-2', 1)
+    self.assertTrue(bot_config)
+    self.assertBotConfigMatchesSwarmingSchema(bot_config)
+    expected = ('eth_switch', '100.107.119.209')
+    self.assertTrue(expected in bot_config.state.items())
+
+    # Also verify works with a slightly different format (newline instead of {})
+    bot_config = configs.GetBotConfig('host-2', 2)
+    self.assertTrue(bot_config)
+    self.assertBotConfigMatchesSwarmingSchema(bot_config)
+    expected = ('eth_switch', '100.107.119.210')
+    self.assertTrue(expected in bot_config.state.items())
+
+  def testGetBotConfig_canGetTestbed(self):
+    sample_yaml = """
+hosts:
+  host-2:
+    host_ip: 2.2.2.2
+    location: 'US-MTV-B45'
+    hosted_devices:
+      - Testbed:
+          dimensions:
+            device_type: chaos_testbed
+            device_ip: 172.22.38.73
+            group: ['chaos_lab']
+          extra_state:
+            anchovy: ['172.22.38.69', '172.22.38.70']
+            biggie: '172.22.38.73'
+            chorizo: ['172.22.38.66', '172.22.38.67']
+            ethernet_switch: '172.22.38.80'
+            linux_client: '172.22.38.81'
+            mushroom: '172.22.38.72'
+            pepperoni: '172.22.38.71'
+            pineapple: ['172.22.38.78', '172.22.38.68']
+            rpi: '172.22.38.79'
+            salami: ['172.22.38.65', '172.22.38.83']
+            steak: ['172.22.38.74', '172.22.38.64']"""
+    configs = devicemap_lib.Configs(yaml.safe_load(sample_yaml))
+    bot_config = configs.GetBotConfig('host-2', 1)
+    self.assertTrue(bot_config)
+    self.assertBotConfigMatchesSwarmingSchema(bot_config)
+    expected = ('biggie', '172.22.38.73')
+    self.assertTrue(expected in bot_config.state.items())
+    expected = ('pineapple', ['172.22.38.78', '172.22.38.68'])
+    self.assertTrue(expected in bot_config.state.items())
+    self.assertIn('location', bot_config.dimensions)
+    self.assertEqual('US-MTV-B45', bot_config.dimensions['location'])
+
+  def testGetBotConfig_undefinedHostRaisesError(self):
+    with self.assertRaises(devicemap_lib.DeviceMapError):
+      self.sample_configs.GetBotConfig('host-doesnt-exist', 1)
+
+  def testGetBotConfig_undefinedDeviceRaisesError(self):
+    with self.assertRaises(devicemap_lib.DeviceMapError):
+      self.sample_configs.GetBotConfig('host-1', 9999)
+
+  def testFactoryResetNoSignedInGroup(self):
+    """Test that FDR flag is set when device is not in the group 'signed_in'."""
+    sample_yaml = """
+hosts:
+  host-2:
+    host_ip: 2.2.2.2
+    hosted_devices:
+      - device:
+          ip: 100.127.32.17
+          group: ['device_performance']"""
+    configs = devicemap_lib.Configs(yaml.safe_load(sample_yaml))
+    bot_config = configs.GetBotConfig('host-2', 1)
+    self.assertTrue(bot_config)
+    self.assertEqual('true', bot_config.dimensions['factory_reset'])
+
+  def testFactoryResetNoGroup(self):
+    """Test that FDR flag is set when device is not explicitly in a group."""
+    sample_yaml = """
+hosts:
+  host-2:
+    host_ip: 2.2.2.2
+    hosted_devices:
+      - device:
+          ip: 100.127.32.17"""
+    configs = devicemap_lib.Configs(yaml.safe_load(sample_yaml))
+    bot_config = configs.GetBotConfig('host-2', 1)
+    self.assertTrue(bot_config)
+    self.assertEqual('true', bot_config.dimensions['factory_reset'])
+
+  def testFactoryResetSignedInGroup(self):
+    """Test that FDR flag is set when device is in the group 'signed_in'."""
+    sample_yaml = """
+hosts:
+  host-2:
+    host_ip: 2.2.2.2
+    hosted_devices:
+      - device:
+          ip: 100.127.32.17
+          group: ['signed_in', 'device_performance']"""
+    configs = devicemap_lib.Configs(yaml.safe_load(sample_yaml))
+    bot_config = configs.GetBotConfig('host-2', 1)
+    self.assertTrue(bot_config)
+    self.assertEqual('false', bot_config.dimensions['factory_reset'])
+
+  def testFactoryResetExplicit(self):
+    """Test that FDR flag is set explicitly specified in config."""
+    sample_yaml = """
+hosts:
+  host-2:
+    host_ip: 2.2.2.2
+    hosted_devices:
+      - device:
+          ip: 100.127.32.17
+          factory_reset: 'false'"""
+    configs = devicemap_lib.Configs(yaml.safe_load(sample_yaml))
+    bot_config = configs.GetBotConfig('host-2', 1)
+    self.assertTrue(bot_config)
+    self.assertEqual('false', bot_config.dimensions['factory_reset'])
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/catatester/devicemap/swarm_bot.systemd.template b/catatester/devicemap/swarm_bot.systemd.template
new file mode 100644
index 0000000..47781f1
--- /dev/null
+++ b/catatester/devicemap/swarm_bot.systemd.template
@@ -0,0 +1,10 @@
+[Unit]
+Description=Startup swarming bot ${bot_name}.
+
+[Service]
+User=${user}
+Group=${group}
+ExecStart=${startup_script_path}
+
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file
diff --git a/catatester/devicemap/swarm_bot_startup.sh.template b/catatester/devicemap/swarm_bot_startup.sh.template
new file mode 100644
index 0000000..7b78fc6
--- /dev/null
+++ b/catatester/devicemap/swarm_bot_startup.sh.template
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+export BOTO_CONFIG=${gsutil_boto}
+export GOOGLE_APPLICATION_CREDENTIALS=${fragglerock_key}
+export HOME_BINARY_DIR=${eurtest_binaries}
+export DEVICEMAP=${devicemap_dir}
+export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${eurtest_binaries}:${gsutil_dir}:${depot_tools_dir}
+. ${venv}/bin/activate
+/usr/bin/env python ${bot_dir}/swarming_bot.zip start_bot
diff --git a/catatester/expand_capabilities.py b/catatester/expand_capabilities.py
new file mode 100644
index 0000000..3b18ec8
--- /dev/null
+++ b/catatester/expand_capabilities.py
@@ -0,0 +1,66 @@
+"""Script to expand capabilities into products.
+
+Example usage:
+  python $0 --capability_list='CAST_AUDIO_RECEIVER,CAST_VIDEO_RECEIVER'
+"""
+import argparse
+import os
+
+from catatester.genfiles import build_config_pb2
+
+from google.protobuf import text_format
+
+
+BUILD_CONFIG = os.path.join(os.path.dirname(__file__),
+                            'configs',
+                            'imported',
+                            'build_config.protoascii')
+
+
+def ParseCmdOptions():
+  """Parses command arguments."""
+  parser = argparse.ArgumentParser()
+  parser.add_argument('--capability_list',
+                      help='Comma-delimited capabilities',
+                      required=True)
+  return parser.parse_args()
+
+
+def GetProductsByCapability(capability):
+  """Returns set of products with the given capability."""
+  print('Capability name: {}'.format(capability))
+  product_set = set()
+  try:
+    capability_index = (
+        build_config_pb2.OtaBuild.Capability.Value(capability))
+    print('Capability Index: {}'.format(capability_index))
+  except ValueError:
+    return product_set
+
+  with open(BUILD_CONFIG) as build:
+    builds = text_format.Merge(build.read(), build_config_pb2.BuildConfig())
+  assert builds
+
+  for target in builds.ota_build:
+    build_target = builds.ota_build[target]
+    if build_target.capabilities:
+      if capability_index in list(build_target.capabilities):
+        product_set.add(build_target.product)
+
+  return product_set
+
+
+def main():
+  assert os.path.isfile(BUILD_CONFIG)
+
+  flags = ParseCmdOptions()
+
+  total_product_set = set()
+  for cap in flags.capability_list.split(','):
+    total_product_set.update(GetProductsByCapability(cap))
+
+  print('All products: {}'.format(sorted(total_product_set)))
+
+
+if __name__ == '__main__':
+  main()
diff --git a/catatester/genfiles/__init__.py b/catatester/genfiles/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/catatester/genfiles/__init__.py
diff --git a/catatester/genfiles/build_config_pb2.py b/catatester/genfiles/build_config_pb2.py
new file mode 100644
index 0000000..d9e1618
--- /dev/null
+++ b/catatester/genfiles/build_config_pb2.py
@@ -0,0 +1,239 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: build_config.proto
+
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='build_config.proto',
+  package='build_config',
+  syntax='proto2',
+  serialized_options=None,
+  serialized_pb=b'\n\x12\x62uild_config.proto\x12\x0c\x62uild_config\"\xb1\x02\n\x08OtaBuild\x12\x14\n\x0c\x62uild_target\x18\x01 \x01(\t\x12\x0f\n\x07product\x18\x02 \x01(\t\x12\x0f\n\x07variant\x18\x03 \x01(\t\x12\x10\n\x08platform\x18\x04 \x01(\t\x12\x16\n\x0epartner_bucket\x18\x05 \x03(\t\x12\x37\n\x0c\x63\x61pabilities\x18\x06 \x03(\x0e\x32!.build_config.OtaBuild.Capability\x12\x13\n\x04test\x18\x07 \x01(\x08:\x05\x66\x61lse\"u\n\nCapability\x12\x08\n\x04NONE\x10\x00\x12\r\n\tASSISTANT\x10\x01\x12\x17\n\x13\x43\x41ST_VIDEO_RECEIVER\x10\x02\x12\x17\n\x13\x43\x41ST_AUDIO_RECEIVER\x10\x03\x12\r\n\tWIFI_2GHZ\x10\x04\x12\r\n\tWIFI_5GHZ\x10\x05\"\x92\x01\n\x0b\x42uildConfig\x12:\n\tota_build\x18\x01 \x03(\x0b\x32\'.build_config.BuildConfig.OtaBuildEntry\x1aG\n\rOtaBuildEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.build_config.OtaBuild:\x02\x38\x01'
+)
+
+
+
+_OTABUILD_CAPABILITY = _descriptor.EnumDescriptor(
+  name='Capability',
+  full_name='build_config.OtaBuild.Capability',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='NONE', index=0, number=0,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='ASSISTANT', index=1, number=1,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='CAST_VIDEO_RECEIVER', index=2, number=2,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='CAST_AUDIO_RECEIVER', index=3, number=3,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='WIFI_2GHZ', index=4, number=4,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='WIFI_5GHZ', index=5, number=5,
+      serialized_options=None,
+      type=None),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=225,
+  serialized_end=342,
+)
+_sym_db.RegisterEnumDescriptor(_OTABUILD_CAPABILITY)
+
+
+_OTABUILD = _descriptor.Descriptor(
+  name='OtaBuild',
+  full_name='build_config.OtaBuild',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='build_target', full_name='build_config.OtaBuild.build_target', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='product', full_name='build_config.OtaBuild.product', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='variant', full_name='build_config.OtaBuild.variant', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='platform', full_name='build_config.OtaBuild.platform', index=3,
+      number=4, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='partner_bucket', full_name='build_config.OtaBuild.partner_bucket', index=4,
+      number=5, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='capabilities', full_name='build_config.OtaBuild.capabilities', index=5,
+      number=6, type=14, cpp_type=8, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='test', full_name='build_config.OtaBuild.test', index=6,
+      number=7, type=8, cpp_type=7, label=1,
+      has_default_value=True, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+    _OTABUILD_CAPABILITY,
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=37,
+  serialized_end=342,
+)
+
+
+_BUILDCONFIG_OTABUILDENTRY = _descriptor.Descriptor(
+  name='OtaBuildEntry',
+  full_name='build_config.BuildConfig.OtaBuildEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='build_config.BuildConfig.OtaBuildEntry.key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='build_config.BuildConfig.OtaBuildEntry.value', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=b'8\001',
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=420,
+  serialized_end=491,
+)
+
+_BUILDCONFIG = _descriptor.Descriptor(
+  name='BuildConfig',
+  full_name='build_config.BuildConfig',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='ota_build', full_name='build_config.BuildConfig.ota_build', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[_BUILDCONFIG_OTABUILDENTRY, ],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=345,
+  serialized_end=491,
+)
+
+_OTABUILD.fields_by_name['capabilities'].enum_type = _OTABUILD_CAPABILITY
+_OTABUILD_CAPABILITY.containing_type = _OTABUILD
+_BUILDCONFIG_OTABUILDENTRY.fields_by_name['value'].message_type = _OTABUILD
+_BUILDCONFIG_OTABUILDENTRY.containing_type = _BUILDCONFIG
+_BUILDCONFIG.fields_by_name['ota_build'].message_type = _BUILDCONFIG_OTABUILDENTRY
+DESCRIPTOR.message_types_by_name['OtaBuild'] = _OTABUILD
+DESCRIPTOR.message_types_by_name['BuildConfig'] = _BUILDCONFIG
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+OtaBuild = _reflection.GeneratedProtocolMessageType('OtaBuild', (_message.Message,), {
+  'DESCRIPTOR' : _OTABUILD,
+  '__module__' : 'build_config_pb2'
+  # @@protoc_insertion_point(class_scope:build_config.OtaBuild)
+  })
+_sym_db.RegisterMessage(OtaBuild)
+
+BuildConfig = _reflection.GeneratedProtocolMessageType('BuildConfig', (_message.Message,), {
+
+  'OtaBuildEntry' : _reflection.GeneratedProtocolMessageType('OtaBuildEntry', (_message.Message,), {
+    'DESCRIPTOR' : _BUILDCONFIG_OTABUILDENTRY,
+    '__module__' : 'build_config_pb2'
+    # @@protoc_insertion_point(class_scope:build_config.BuildConfig.OtaBuildEntry)
+    })
+  ,
+  'DESCRIPTOR' : _BUILDCONFIG,
+  '__module__' : 'build_config_pb2'
+  # @@protoc_insertion_point(class_scope:build_config.BuildConfig)
+  })
+_sym_db.RegisterMessage(BuildConfig)
+_sym_db.RegisterMessage(BuildConfig.OtaBuildEntry)
+
+
+_BUILDCONFIG_OTABUILDENTRY._options = None
+# @@protoc_insertion_point(module_scope)
diff --git a/catatester/genfiles/continuous_tests_pb2.py b/catatester/genfiles/continuous_tests_pb2.py
new file mode 100644
index 0000000..2672ef0
--- /dev/null
+++ b/catatester/genfiles/continuous_tests_pb2.py
@@ -0,0 +1,1206 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: continuous_tests.proto
+
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='continuous_tests.proto',
+  package='continuous_tests',
+  syntax='proto2',
+  serialized_options=None,
+  serialized_pb=b'\n\x16\x63ontinuous_tests.proto\x12\x10\x63ontinuous_tests\"\xf5\x03\n\x0c\x42uildTargets\x12?\n\x0c\x62ranch_types\x18\x01 \x01(\x0e\x32).continuous_tests.BuildTargets.BranchType\x12\x15\n\rproduct_types\x18\x02 \x03(\t\x12\x10\n\x08products\x18\x03 \x03(\t\x12\x38\n\x08variants\x18\x04 \x01(\x0e\x32&.continuous_tests.BuildTargets.Variant\x12\x15\n\rbuilder_names\x18\x05 \x03(\t\x12\x37\n\x08\x62ranches\x18\x06 \x03(\x0b\x32%.continuous_tests.BuildTargets.Branch\x1a\x61\n\x06\x42ranch\x12\x39\n\x04type\x18\x01 \x01(\x0e\x32).continuous_tests.BuildTargets.BranchTypeH\x00\x12\x0e\n\x04name\x18\x02 \x01(\tH\x00\x42\x0c\n\nBranchSpec\".\n\nBranchType\x12\x07\n\x03\x41LL\x10\x00\x12\n\n\x06MASTER\x10\x01\x12\x0b\n\x07RELEASE\x10\x02\"^\n\x07Variant\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x07\n\x03\x45NG\x10\x01\x12\x08\n\x04USER\x10\x02\x12\x07\n\x03\x41RM\x10\x03\x12\x07\n\x03X86\x10\x04\x12\x07\n\x03X64\x10\x05\x12\t\n\x05\x41RM64\x10\x06\x12\r\n\tUSERDEBUG\x10\x07\"\x99\x01\n\x08Schedule\x12\x0c\n\x04\x63ron\x18\x01 \x01(\t\x12\x31\n\x06policy\x18\x02 \x01(\x0e\x32!.continuous_tests.Schedule.Policy\"L\n\x06Policy\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x13\n\x0fLATEST_UNTESTED\x10\x01\x12\n\n\x06LATEST\x10\x02\x12\x14\n\x10LATEST_MINUS_ONE\x10\x03\"6\n\x0e\x42otRequirement\x12\x15\n\rbot_dimension\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\":\n\x0c\x43\x61stOsModule\x12\x14\n\x0c\x63ipd_package\x18\x01 \x01(\t\x12\x14\n\x0c\x63ipd_version\x18\x02 \x01(\t\"\xc7\x05\n\nTestTarget\x12\x0e\n\x06\x63onfig\x18\x01 \x01(\t\x12\x12\n\ntest_cases\x18\x02 \x03(\t\x12\x13\n\x0b\x62inary_name\x18\x03 \x01(\t\x12\x37\n\x08priority\x18\x04 \x01(\x0e\x32%.continuous_tests.TestTarget.Priority\x12\x15\n\x07\x65nabled\x18\x05 \x01(\x08:\x04true\x12\x33\n\x06recipe\x18\x06 \x01(\x0e\x32#.continuous_tests.TestTarget.Recipe\x12\x1d\n\x11test_timeout_mins\x18\x07 \x01(\x05:\x02\x36\x30\x12\x13\n\x0btest_groups\x18\x08 \x03(\t\x12>\n\x14testbed_requirements\x18\t \x03(\x0b\x32 .continuous_tests.BotRequirement\x12\x1f\n\rcipd_refs_key\x18\n \x01(\t:\x08internal\x12\x10\n\x08\x62ot_pool\x18\x0b \x01(\t\x12\x36\n\x0e\x63\x61stos_modules\x18\x0c \x03(\x0b\x32\x1e.continuous_tests.CastOsModule\x12\x1f\n\x17skip_default_dimensions\x18\r \x01(\x08\"\xb4\x01\n\x06Recipe\x12\x0f\n\x0bHOME_RUNNER\x10\x00\x12\x1b\n\x17\x44\x45VICE_UNIT_TEST_RUNNER\x10\x01\x12\x1a\n\x16OTA_INSTALLABLE_RUNNER\x10\x02\x12\x17\n\x13LIBASSISTANT_RUNNER\x10\x03\x12\x18\n\x14\x46UCHSIA_GUNIT_RUNNER\x10\x04\x12\x12\n\x0e\x46UCHSIA_RUNNER\x10\x05\x12\x19\n\x15VIRTUAL_DEVICE_RUNNER\x10\x06\"D\n\x08Priority\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x0c\n\x08\x43RITICAL\x10\x01\x12\n\n\x06MEDIUM\x10\x02\x12\x08\n\x04HIGH\x10\x03\x12\x07\n\x03LOW\x10\x04\"z\n\x0e\x43ontinuousTest\x12\x31\n\x0btest_target\x18\x01 \x01(\x0b\x32\x1c.continuous_tests.TestTarget\x12\x35\n\rbuild_targets\x18\x02 \x03(\x0b\x32\x1e.continuous_tests.BuildTargets\"\x97\x02\n\rScheduledTest\x12\x31\n\x0btest_target\x18\x01 \x01(\x0b\x32\x1c.continuous_tests.TestTarget\x12\x35\n\rbuild_targets\x18\x02 \x03(\x0b\x32\x1e.continuous_tests.BuildTargets\x12,\n\x08schedule\x18\x03 \x01(\x0b\x32\x1a.continuous_tests.Schedule\x12\x10\n\x08\x62ranches\x18\x04 \x03(\t\x12\x16\n\x04repo\x18\x05 \x01(\t:\x08internal\x12\x44\n\x0e\x62uild_platform\x18\x06 \x01(\x0e\x32\x1f.continuous_tests.BuildPlatform:\x0b\x43\x41TABUILDER\"\xcc\t\n\x15\x43ontinuousTestsConfig\x12\x19\n\x11test_configs_root\x18\x01 \x01(\t\x12\x39\n\x0f\x63ontinuous_test\x18\x02 \x03(\x0b\x32 .continuous_tests.ContinuousTest\x12\x37\n\x0escheduled_test\x18\x03 \x03(\x0b\x32\x1f.continuous_tests.ScheduledTest\x12I\n\x16\x62ranch_type_priorities\x18\x04 \x03(\x0e\x32).continuous_tests.BuildTargets.BranchType\x12\x19\n\x11\x64isabled_branches\x18\x05 \x03(\t\x12Z\n\x12\x62uild_restrictions\x18\x06 \x03(\x0b\x32>.continuous_tests.ContinuousTestsConfig.BuildRestrictionsEntry\x12\x17\n\x0ftest_group_tags\x18\x07 \x03(\t\x12R\n\x0e\x64\x65vice_modules\x18\x08 \x03(\x0b\x32:.continuous_tests.ContinuousTestsConfig.DeviceModulesEntry\x12H\n\tcipd_refs\x18\t \x03(\x0b\x32\x35.continuous_tests.ContinuousTestsConfig.CipdRefsEntry\x1a\x9e\x01\n\x11\x42uildRestrictions\x12G\n\x14\x61llowed_branch_types\x18\x01 \x03(\x0e\x32).continuous_tests.BuildTargets.BranchType\x12@\n\x10\x61llowed_variants\x18\x02 \x03(\x0e\x32&.continuous_tests.BuildTargets.Variant\x1as\n\x16\x42uildRestrictionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12H\n\x05value\x18\x02 \x01(\x0b\x32\x39.continuous_tests.ContinuousTestsConfig.BuildRestrictions:\x02\x38\x01\x1a;\n\x0c\x44\x65viceModule\x12\x15\n\rdevice_module\x18\x01 \x01(\t\x12\x14\n\x0c\x64\x65vice_class\x18\x02 \x01(\t\x1aj\n\x12\x44\x65viceModulesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x43\n\x05value\x18\x02 \x01(\x0b\x32\x34.continuous_tests.ContinuousTestsConfig.DeviceModule:\x02\x38\x01\x1a>\n\x07\x43ipdRef\x12\x14\n\x0cpackage_name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x0c\n\x04path\x18\x03 \x01(\t\x1aI\n\x08\x43ipdRefs\x12=\n\x04refs\x18\x01 \x03(\x0b\x32/.continuous_tests.ContinuousTestsConfig.CipdRef\x1a\x61\n\rCipdRefsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12?\n\x05value\x18\x02 \x01(\x0b\x32\x30.continuous_tests.ContinuousTestsConfig.CipdRefs:\x02\x38\x01*@\n\rBuildPlatform\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x0f\n\x0b\x43\x41TABUILDER\x10\x01\x12\x11\n\rANDROID_BUILD\x10\x02'
+)
+
+_BUILDPLATFORM = _descriptor.EnumDescriptor(
+  name='BuildPlatform',
+  full_name='continuous_tests.BuildPlatform',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='DEFAULT', index=0, number=0,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='CATABUILDER', index=1, number=1,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='ANDROID_BUILD', index=2, number=2,
+      serialized_options=None,
+      type=None),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=3171,
+  serialized_end=3235,
+)
+_sym_db.RegisterEnumDescriptor(_BUILDPLATFORM)
+
+BuildPlatform = enum_type_wrapper.EnumTypeWrapper(_BUILDPLATFORM)
+DEFAULT = 0
+CATABUILDER = 1
+ANDROID_BUILD = 2
+
+
+_BUILDTARGETS_BRANCHTYPE = _descriptor.EnumDescriptor(
+  name='BranchType',
+  full_name='continuous_tests.BuildTargets.BranchType',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='ALL', index=0, number=0,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='MASTER', index=1, number=1,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='RELEASE', index=2, number=2,
+      serialized_options=None,
+      type=None),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=404,
+  serialized_end=450,
+)
+_sym_db.RegisterEnumDescriptor(_BUILDTARGETS_BRANCHTYPE)
+
+_BUILDTARGETS_VARIANT = _descriptor.EnumDescriptor(
+  name='Variant',
+  full_name='continuous_tests.BuildTargets.Variant',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='DEFAULT', index=0, number=0,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='ENG', index=1, number=1,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='USER', index=2, number=2,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='ARM', index=3, number=3,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='X86', index=4, number=4,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='X64', index=5, number=5,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='ARM64', index=6, number=6,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='USERDEBUG', index=7, number=7,
+      serialized_options=None,
+      type=None),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=452,
+  serialized_end=546,
+)
+_sym_db.RegisterEnumDescriptor(_BUILDTARGETS_VARIANT)
+
+_SCHEDULE_POLICY = _descriptor.EnumDescriptor(
+  name='Policy',
+  full_name='continuous_tests.Schedule.Policy',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='DEFAULT', index=0, number=0,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='LATEST_UNTESTED', index=1, number=1,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='LATEST', index=2, number=2,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='LATEST_MINUS_ONE', index=3, number=3,
+      serialized_options=None,
+      type=None),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=626,
+  serialized_end=702,
+)
+_sym_db.RegisterEnumDescriptor(_SCHEDULE_POLICY)
+
+_TESTTARGET_RECIPE = _descriptor.EnumDescriptor(
+  name='Recipe',
+  full_name='continuous_tests.TestTarget.Recipe',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='HOME_RUNNER', index=0, number=0,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='DEVICE_UNIT_TEST_RUNNER', index=1, number=1,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='OTA_INSTALLABLE_RUNNER', index=2, number=2,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='LIBASSISTANT_RUNNER', index=3, number=3,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='FUCHSIA_GUNIT_RUNNER', index=4, number=4,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='FUCHSIA_RUNNER', index=5, number=5,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='VIRTUAL_DEVICE_RUNNER', index=6, number=6,
+      serialized_options=None,
+      type=None),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=1282,
+  serialized_end=1462,
+)
+_sym_db.RegisterEnumDescriptor(_TESTTARGET_RECIPE)
+
+_TESTTARGET_PRIORITY = _descriptor.EnumDescriptor(
+  name='Priority',
+  full_name='continuous_tests.TestTarget.Priority',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='DEFAULT', index=0, number=0,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='CRITICAL', index=1, number=1,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='MEDIUM', index=2, number=2,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='HIGH', index=3, number=3,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='LOW', index=4, number=4,
+      serialized_options=None,
+      type=None),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=1464,
+  serialized_end=1532,
+)
+_sym_db.RegisterEnumDescriptor(_TESTTARGET_PRIORITY)
+
+
+_BUILDTARGETS_BRANCH = _descriptor.Descriptor(
+  name='Branch',
+  full_name='continuous_tests.BuildTargets.Branch',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='type', full_name='continuous_tests.BuildTargets.Branch.type', index=0,
+      number=1, type=14, cpp_type=8, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='name', full_name='continuous_tests.BuildTargets.Branch.name', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+    _descriptor.OneofDescriptor(
+      name='BranchSpec', full_name='continuous_tests.BuildTargets.Branch.BranchSpec',
+      index=0, containing_type=None, fields=[]),
+  ],
+  serialized_start=305,
+  serialized_end=402,
+)
+
+_BUILDTARGETS = _descriptor.Descriptor(
+  name='BuildTargets',
+  full_name='continuous_tests.BuildTargets',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='branch_types', full_name='continuous_tests.BuildTargets.branch_types', index=0,
+      number=1, type=14, cpp_type=8, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='product_types', full_name='continuous_tests.BuildTargets.product_types', index=1,
+      number=2, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='products', full_name='continuous_tests.BuildTargets.products', index=2,
+      number=3, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='variants', full_name='continuous_tests.BuildTargets.variants', index=3,
+      number=4, type=14, cpp_type=8, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='builder_names', full_name='continuous_tests.BuildTargets.builder_names', index=4,
+      number=5, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='branches', full_name='continuous_tests.BuildTargets.branches', index=5,
+      number=6, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[_BUILDTARGETS_BRANCH, ],
+  enum_types=[
+    _BUILDTARGETS_BRANCHTYPE,
+    _BUILDTARGETS_VARIANT,
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=45,
+  serialized_end=546,
+)
+
+
+_SCHEDULE = _descriptor.Descriptor(
+  name='Schedule',
+  full_name='continuous_tests.Schedule',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='cron', full_name='continuous_tests.Schedule.cron', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='policy', full_name='continuous_tests.Schedule.policy', index=1,
+      number=2, type=14, cpp_type=8, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+    _SCHEDULE_POLICY,
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=549,
+  serialized_end=702,
+)
+
+
+_BOTREQUIREMENT = _descriptor.Descriptor(
+  name='BotRequirement',
+  full_name='continuous_tests.BotRequirement',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='bot_dimension', full_name='continuous_tests.BotRequirement.bot_dimension', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='continuous_tests.BotRequirement.value', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=704,
+  serialized_end=758,
+)
+
+
+_CASTOSMODULE = _descriptor.Descriptor(
+  name='CastOsModule',
+  full_name='continuous_tests.CastOsModule',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='cipd_package', full_name='continuous_tests.CastOsModule.cipd_package', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='cipd_version', full_name='continuous_tests.CastOsModule.cipd_version', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=760,
+  serialized_end=818,
+)
+
+
+_TESTTARGET = _descriptor.Descriptor(
+  name='TestTarget',
+  full_name='continuous_tests.TestTarget',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='config', full_name='continuous_tests.TestTarget.config', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='test_cases', full_name='continuous_tests.TestTarget.test_cases', index=1,
+      number=2, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='binary_name', full_name='continuous_tests.TestTarget.binary_name', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='priority', full_name='continuous_tests.TestTarget.priority', index=3,
+      number=4, type=14, cpp_type=8, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='enabled', full_name='continuous_tests.TestTarget.enabled', index=4,
+      number=5, type=8, cpp_type=7, label=1,
+      has_default_value=True, default_value=True,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='recipe', full_name='continuous_tests.TestTarget.recipe', index=5,
+      number=6, type=14, cpp_type=8, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='test_timeout_mins', full_name='continuous_tests.TestTarget.test_timeout_mins', index=6,
+      number=7, type=5, cpp_type=1, label=1,
+      has_default_value=True, default_value=60,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='test_groups', full_name='continuous_tests.TestTarget.test_groups', index=7,
+      number=8, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='testbed_requirements', full_name='continuous_tests.TestTarget.testbed_requirements', index=8,
+      number=9, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='cipd_refs_key', full_name='continuous_tests.TestTarget.cipd_refs_key', index=9,
+      number=10, type=9, cpp_type=9, label=1,
+      has_default_value=True, default_value=b"internal".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='bot_pool', full_name='continuous_tests.TestTarget.bot_pool', index=10,
+      number=11, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='castos_modules', full_name='continuous_tests.TestTarget.castos_modules', index=11,
+      number=12, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='skip_default_dimensions', full_name='continuous_tests.TestTarget.skip_default_dimensions', index=12,
+      number=13, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+    _TESTTARGET_RECIPE,
+    _TESTTARGET_PRIORITY,
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=821,
+  serialized_end=1532,
+)
+
+
+_CONTINUOUSTEST = _descriptor.Descriptor(
+  name='ContinuousTest',
+  full_name='continuous_tests.ContinuousTest',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='test_target', full_name='continuous_tests.ContinuousTest.test_target', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='build_targets', full_name='continuous_tests.ContinuousTest.build_targets', index=1,
+      number=2, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1534,
+  serialized_end=1656,
+)
+
+
+_SCHEDULEDTEST = _descriptor.Descriptor(
+  name='ScheduledTest',
+  full_name='continuous_tests.ScheduledTest',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='test_target', full_name='continuous_tests.ScheduledTest.test_target', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='build_targets', full_name='continuous_tests.ScheduledTest.build_targets', index=1,
+      number=2, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='schedule', full_name='continuous_tests.ScheduledTest.schedule', index=2,
+      number=3, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='branches', full_name='continuous_tests.ScheduledTest.branches', index=3,
+      number=4, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='repo', full_name='continuous_tests.ScheduledTest.repo', index=4,
+      number=5, type=9, cpp_type=9, label=1,
+      has_default_value=True, default_value=b"internal".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='build_platform', full_name='continuous_tests.ScheduledTest.build_platform', index=5,
+      number=6, type=14, cpp_type=8, label=1,
+      has_default_value=True, default_value=1,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1659,
+  serialized_end=1938,
+)
+
+
+_CONTINUOUSTESTSCONFIG_BUILDRESTRICTIONS = _descriptor.Descriptor(
+  name='BuildRestrictions',
+  full_name='continuous_tests.ContinuousTestsConfig.BuildRestrictions',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='allowed_branch_types', full_name='continuous_tests.ContinuousTestsConfig.BuildRestrictions.allowed_branch_types', index=0,
+      number=1, type=14, cpp_type=8, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='allowed_variants', full_name='continuous_tests.ContinuousTestsConfig.BuildRestrictions.allowed_variants', index=1,
+      number=2, type=14, cpp_type=8, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=2487,
+  serialized_end=2645,
+)
+
+_CONTINUOUSTESTSCONFIG_BUILDRESTRICTIONSENTRY = _descriptor.Descriptor(
+  name='BuildRestrictionsEntry',
+  full_name='continuous_tests.ContinuousTestsConfig.BuildRestrictionsEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='continuous_tests.ContinuousTestsConfig.BuildRestrictionsEntry.key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='continuous_tests.ContinuousTestsConfig.BuildRestrictionsEntry.value', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=b'8\001',
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=2647,
+  serialized_end=2762,
+)
+
+_CONTINUOUSTESTSCONFIG_DEVICEMODULE = _descriptor.Descriptor(
+  name='DeviceModule',
+  full_name='continuous_tests.ContinuousTestsConfig.DeviceModule',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='device_module', full_name='continuous_tests.ContinuousTestsConfig.DeviceModule.device_module', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='device_class', full_name='continuous_tests.ContinuousTestsConfig.DeviceModule.device_class', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=2764,
+  serialized_end=2823,
+)
+
+_CONTINUOUSTESTSCONFIG_DEVICEMODULESENTRY = _descriptor.Descriptor(
+  name='DeviceModulesEntry',
+  full_name='continuous_tests.ContinuousTestsConfig.DeviceModulesEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='continuous_tests.ContinuousTestsConfig.DeviceModulesEntry.key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='continuous_tests.ContinuousTestsConfig.DeviceModulesEntry.value', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=b'8\001',
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=2825,
+  serialized_end=2931,
+)
+
+_CONTINUOUSTESTSCONFIG_CIPDREF = _descriptor.Descriptor(
+  name='CipdRef',
+  full_name='continuous_tests.ContinuousTestsConfig.CipdRef',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='package_name', full_name='continuous_tests.ContinuousTestsConfig.CipdRef.package_name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='version', full_name='continuous_tests.ContinuousTestsConfig.CipdRef.version', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='path', full_name='continuous_tests.ContinuousTestsConfig.CipdRef.path', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=2933,
+  serialized_end=2995,
+)
+
+_CONTINUOUSTESTSCONFIG_CIPDREFS = _descriptor.Descriptor(
+  name='CipdRefs',
+  full_name='continuous_tests.ContinuousTestsConfig.CipdRefs',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='refs', full_name='continuous_tests.ContinuousTestsConfig.CipdRefs.refs', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=2997,
+  serialized_end=3070,
+)
+
+_CONTINUOUSTESTSCONFIG_CIPDREFSENTRY = _descriptor.Descriptor(
+  name='CipdRefsEntry',
+  full_name='continuous_tests.ContinuousTestsConfig.CipdRefsEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='continuous_tests.ContinuousTestsConfig.CipdRefsEntry.key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='continuous_tests.ContinuousTestsConfig.CipdRefsEntry.value', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=b'8\001',
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=3072,
+  serialized_end=3169,
+)
+
+_CONTINUOUSTESTSCONFIG = _descriptor.Descriptor(
+  name='ContinuousTestsConfig',
+  full_name='continuous_tests.ContinuousTestsConfig',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='test_configs_root', full_name='continuous_tests.ContinuousTestsConfig.test_configs_root', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='continuous_test', full_name='continuous_tests.ContinuousTestsConfig.continuous_test', index=1,
+      number=2, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='scheduled_test', full_name='continuous_tests.ContinuousTestsConfig.scheduled_test', index=2,
+      number=3, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='branch_type_priorities', full_name='continuous_tests.ContinuousTestsConfig.branch_type_priorities', index=3,
+      number=4, type=14, cpp_type=8, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='disabled_branches', full_name='continuous_tests.ContinuousTestsConfig.disabled_branches', index=4,
+      number=5, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='build_restrictions', full_name='continuous_tests.ContinuousTestsConfig.build_restrictions', index=5,
+      number=6, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='test_group_tags', full_name='continuous_tests.ContinuousTestsConfig.test_group_tags', index=6,
+      number=7, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='device_modules', full_name='continuous_tests.ContinuousTestsConfig.device_modules', index=7,
+      number=8, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='cipd_refs', full_name='continuous_tests.ContinuousTestsConfig.cipd_refs', index=8,
+      number=9, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[_CONTINUOUSTESTSCONFIG_BUILDRESTRICTIONS, _CONTINUOUSTESTSCONFIG_BUILDRESTRICTIONSENTRY, _CONTINUOUSTESTSCONFIG_DEVICEMODULE, _CONTINUOUSTESTSCONFIG_DEVICEMODULESENTRY, _CONTINUOUSTESTSCONFIG_CIPDREF, _CONTINUOUSTESTSCONFIG_CIPDREFS, _CONTINUOUSTESTSCONFIG_CIPDREFSENTRY, ],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1941,
+  serialized_end=3169,
+)
+
+_BUILDTARGETS_BRANCH.fields_by_name['type'].enum_type = _BUILDTARGETS_BRANCHTYPE
+_BUILDTARGETS_BRANCH.containing_type = _BUILDTARGETS
+_BUILDTARGETS_BRANCH.oneofs_by_name['BranchSpec'].fields.append(
+  _BUILDTARGETS_BRANCH.fields_by_name['type'])
+_BUILDTARGETS_BRANCH.fields_by_name['type'].containing_oneof = _BUILDTARGETS_BRANCH.oneofs_by_name['BranchSpec']
+_BUILDTARGETS_BRANCH.oneofs_by_name['BranchSpec'].fields.append(
+  _BUILDTARGETS_BRANCH.fields_by_name['name'])
+_BUILDTARGETS_BRANCH.fields_by_name['name'].containing_oneof = _BUILDTARGETS_BRANCH.oneofs_by_name['BranchSpec']
+_BUILDTARGETS.fields_by_name['branch_types'].enum_type = _BUILDTARGETS_BRANCHTYPE
+_BUILDTARGETS.fields_by_name['variants'].enum_type = _BUILDTARGETS_VARIANT
+_BUILDTARGETS.fields_by_name['branches'].message_type = _BUILDTARGETS_BRANCH
+_BUILDTARGETS_BRANCHTYPE.containing_type = _BUILDTARGETS
+_BUILDTARGETS_VARIANT.containing_type = _BUILDTARGETS
+_SCHEDULE.fields_by_name['policy'].enum_type = _SCHEDULE_POLICY
+_SCHEDULE_POLICY.containing_type = _SCHEDULE
+_TESTTARGET.fields_by_name['priority'].enum_type = _TESTTARGET_PRIORITY
+_TESTTARGET.fields_by_name['recipe'].enum_type = _TESTTARGET_RECIPE
+_TESTTARGET.fields_by_name['testbed_requirements'].message_type = _BOTREQUIREMENT
+_TESTTARGET.fields_by_name['castos_modules'].message_type = _CASTOSMODULE
+_TESTTARGET_RECIPE.containing_type = _TESTTARGET
+_TESTTARGET_PRIORITY.containing_type = _TESTTARGET
+_CONTINUOUSTEST.fields_by_name['test_target'].message_type = _TESTTARGET
+_CONTINUOUSTEST.fields_by_name['build_targets'].message_type = _BUILDTARGETS
+_SCHEDULEDTEST.fields_by_name['test_target'].message_type = _TESTTARGET
+_SCHEDULEDTEST.fields_by_name['build_targets'].message_type = _BUILDTARGETS
+_SCHEDULEDTEST.fields_by_name['schedule'].message_type = _SCHEDULE
+_SCHEDULEDTEST.fields_by_name['build_platform'].enum_type = _BUILDPLATFORM
+_CONTINUOUSTESTSCONFIG_BUILDRESTRICTIONS.fields_by_name['allowed_branch_types'].enum_type = _BUILDTARGETS_BRANCHTYPE
+_CONTINUOUSTESTSCONFIG_BUILDRESTRICTIONS.fields_by_name['allowed_variants'].enum_type = _BUILDTARGETS_VARIANT
+_CONTINUOUSTESTSCONFIG_BUILDRESTRICTIONS.containing_type = _CONTINUOUSTESTSCONFIG
+_CONTINUOUSTESTSCONFIG_BUILDRESTRICTIONSENTRY.fields_by_name['value'].message_type = _CONTINUOUSTESTSCONFIG_BUILDRESTRICTIONS
+_CONTINUOUSTESTSCONFIG_BUILDRESTRICTIONSENTRY.containing_type = _CONTINUOUSTESTSCONFIG
+_CONTINUOUSTESTSCONFIG_DEVICEMODULE.containing_type = _CONTINUOUSTESTSCONFIG
+_CONTINUOUSTESTSCONFIG_DEVICEMODULESENTRY.fields_by_name['value'].message_type = _CONTINUOUSTESTSCONFIG_DEVICEMODULE
+_CONTINUOUSTESTSCONFIG_DEVICEMODULESENTRY.containing_type = _CONTINUOUSTESTSCONFIG
+_CONTINUOUSTESTSCONFIG_CIPDREF.containing_type = _CONTINUOUSTESTSCONFIG
+_CONTINUOUSTESTSCONFIG_CIPDREFS.fields_by_name['refs'].message_type = _CONTINUOUSTESTSCONFIG_CIPDREF
+_CONTINUOUSTESTSCONFIG_CIPDREFS.containing_type = _CONTINUOUSTESTSCONFIG
+_CONTINUOUSTESTSCONFIG_CIPDREFSENTRY.fields_by_name['value'].message_type = _CONTINUOUSTESTSCONFIG_CIPDREFS
+_CONTINUOUSTESTSCONFIG_CIPDREFSENTRY.containing_type = _CONTINUOUSTESTSCONFIG
+_CONTINUOUSTESTSCONFIG.fields_by_name['continuous_test'].message_type = _CONTINUOUSTEST
+_CONTINUOUSTESTSCONFIG.fields_by_name['scheduled_test'].message_type = _SCHEDULEDTEST
+_CONTINUOUSTESTSCONFIG.fields_by_name['branch_type_priorities'].enum_type = _BUILDTARGETS_BRANCHTYPE
+_CONTINUOUSTESTSCONFIG.fields_by_name['build_restrictions'].message_type = _CONTINUOUSTESTSCONFIG_BUILDRESTRICTIONSENTRY
+_CONTINUOUSTESTSCONFIG.fields_by_name['device_modules'].message_type = _CONTINUOUSTESTSCONFIG_DEVICEMODULESENTRY
+_CONTINUOUSTESTSCONFIG.fields_by_name['cipd_refs'].message_type = _CONTINUOUSTESTSCONFIG_CIPDREFSENTRY
+DESCRIPTOR.message_types_by_name['BuildTargets'] = _BUILDTARGETS
+DESCRIPTOR.message_types_by_name['Schedule'] = _SCHEDULE
+DESCRIPTOR.message_types_by_name['BotRequirement'] = _BOTREQUIREMENT
+DESCRIPTOR.message_types_by_name['CastOsModule'] = _CASTOSMODULE
+DESCRIPTOR.message_types_by_name['TestTarget'] = _TESTTARGET
+DESCRIPTOR.message_types_by_name['ContinuousTest'] = _CONTINUOUSTEST
+DESCRIPTOR.message_types_by_name['ScheduledTest'] = _SCHEDULEDTEST
+DESCRIPTOR.message_types_by_name['ContinuousTestsConfig'] = _CONTINUOUSTESTSCONFIG
+DESCRIPTOR.enum_types_by_name['BuildPlatform'] = _BUILDPLATFORM
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+BuildTargets = _reflection.GeneratedProtocolMessageType('BuildTargets', (_message.Message,), {
+
+  'Branch' : _reflection.GeneratedProtocolMessageType('Branch', (_message.Message,), {
+    'DESCRIPTOR' : _BUILDTARGETS_BRANCH,
+    '__module__' : 'continuous_tests_pb2'
+    # @@protoc_insertion_point(class_scope:continuous_tests.BuildTargets.Branch)
+    })
+  ,
+  'DESCRIPTOR' : _BUILDTARGETS,
+  '__module__' : 'continuous_tests_pb2'
+  # @@protoc_insertion_point(class_scope:continuous_tests.BuildTargets)
+  })
+_sym_db.RegisterMessage(BuildTargets)
+_sym_db.RegisterMessage(BuildTargets.Branch)
+
+Schedule = _reflection.GeneratedProtocolMessageType('Schedule', (_message.Message,), {
+  'DESCRIPTOR' : _SCHEDULE,
+  '__module__' : 'continuous_tests_pb2'
+  # @@protoc_insertion_point(class_scope:continuous_tests.Schedule)
+  })
+_sym_db.RegisterMessage(Schedule)
+
+BotRequirement = _reflection.GeneratedProtocolMessageType('BotRequirement', (_message.Message,), {
+  'DESCRIPTOR' : _BOTREQUIREMENT,
+  '__module__' : 'continuous_tests_pb2'
+  # @@protoc_insertion_point(class_scope:continuous_tests.BotRequirement)
+  })
+_sym_db.RegisterMessage(BotRequirement)
+
+CastOsModule = _reflection.GeneratedProtocolMessageType('CastOsModule', (_message.Message,), {
+  'DESCRIPTOR' : _CASTOSMODULE,
+  '__module__' : 'continuous_tests_pb2'
+  # @@protoc_insertion_point(class_scope:continuous_tests.CastOsModule)
+  })
+_sym_db.RegisterMessage(CastOsModule)
+
+TestTarget = _reflection.GeneratedProtocolMessageType('TestTarget', (_message.Message,), {
+  'DESCRIPTOR' : _TESTTARGET,
+  '__module__' : 'continuous_tests_pb2'
+  # @@protoc_insertion_point(class_scope:continuous_tests.TestTarget)
+  })
+_sym_db.RegisterMessage(TestTarget)
+
+ContinuousTest = _reflection.GeneratedProtocolMessageType('ContinuousTest', (_message.Message,), {
+  'DESCRIPTOR' : _CONTINUOUSTEST,
+  '__module__' : 'continuous_tests_pb2'
+  # @@protoc_insertion_point(class_scope:continuous_tests.ContinuousTest)
+  })
+_sym_db.RegisterMessage(ContinuousTest)
+
+ScheduledTest = _reflection.GeneratedProtocolMessageType('ScheduledTest', (_message.Message,), {
+  'DESCRIPTOR' : _SCHEDULEDTEST,
+  '__module__' : 'continuous_tests_pb2'
+  # @@protoc_insertion_point(class_scope:continuous_tests.ScheduledTest)
+  })
+_sym_db.RegisterMessage(ScheduledTest)
+
+ContinuousTestsConfig = _reflection.GeneratedProtocolMessageType('ContinuousTestsConfig', (_message.Message,), {
+
+  'BuildRestrictions' : _reflection.GeneratedProtocolMessageType('BuildRestrictions', (_message.Message,), {
+    'DESCRIPTOR' : _CONTINUOUSTESTSCONFIG_BUILDRESTRICTIONS,
+    '__module__' : 'continuous_tests_pb2'
+    # @@protoc_insertion_point(class_scope:continuous_tests.ContinuousTestsConfig.BuildRestrictions)
+    })
+  ,
+
+  'BuildRestrictionsEntry' : _reflection.GeneratedProtocolMessageType('BuildRestrictionsEntry', (_message.Message,), {
+    'DESCRIPTOR' : _CONTINUOUSTESTSCONFIG_BUILDRESTRICTIONSENTRY,
+    '__module__' : 'continuous_tests_pb2'
+    # @@protoc_insertion_point(class_scope:continuous_tests.ContinuousTestsConfig.BuildRestrictionsEntry)
+    })
+  ,
+
+  'DeviceModule' : _reflection.GeneratedProtocolMessageType('DeviceModule', (_message.Message,), {
+    'DESCRIPTOR' : _CONTINUOUSTESTSCONFIG_DEVICEMODULE,
+    '__module__' : 'continuous_tests_pb2'
+    # @@protoc_insertion_point(class_scope:continuous_tests.ContinuousTestsConfig.DeviceModule)
+    })
+  ,
+
+  'DeviceModulesEntry' : _reflection.GeneratedProtocolMessageType('DeviceModulesEntry', (_message.Message,), {
+    'DESCRIPTOR' : _CONTINUOUSTESTSCONFIG_DEVICEMODULESENTRY,
+    '__module__' : 'continuous_tests_pb2'
+    # @@protoc_insertion_point(class_scope:continuous_tests.ContinuousTestsConfig.DeviceModulesEntry)
+    })
+  ,
+
+  'CipdRef' : _reflection.GeneratedProtocolMessageType('CipdRef', (_message.Message,), {
+    'DESCRIPTOR' : _CONTINUOUSTESTSCONFIG_CIPDREF,
+    '__module__' : 'continuous_tests_pb2'
+    # @@protoc_insertion_point(class_scope:continuous_tests.ContinuousTestsConfig.CipdRef)
+    })
+  ,
+
+  'CipdRefs' : _reflection.GeneratedProtocolMessageType('CipdRefs', (_message.Message,), {
+    'DESCRIPTOR' : _CONTINUOUSTESTSCONFIG_CIPDREFS,
+    '__module__' : 'continuous_tests_pb2'
+    # @@protoc_insertion_point(class_scope:continuous_tests.ContinuousTestsConfig.CipdRefs)
+    })
+  ,
+
+  'CipdRefsEntry' : _reflection.GeneratedProtocolMessageType('CipdRefsEntry', (_message.Message,), {
+    'DESCRIPTOR' : _CONTINUOUSTESTSCONFIG_CIPDREFSENTRY,
+    '__module__' : 'continuous_tests_pb2'
+    # @@protoc_insertion_point(class_scope:continuous_tests.ContinuousTestsConfig.CipdRefsEntry)
+    })
+  ,
+  'DESCRIPTOR' : _CONTINUOUSTESTSCONFIG,
+  '__module__' : 'continuous_tests_pb2'
+  # @@protoc_insertion_point(class_scope:continuous_tests.ContinuousTestsConfig)
+  })
+_sym_db.RegisterMessage(ContinuousTestsConfig)
+_sym_db.RegisterMessage(ContinuousTestsConfig.BuildRestrictions)
+_sym_db.RegisterMessage(ContinuousTestsConfig.BuildRestrictionsEntry)
+_sym_db.RegisterMessage(ContinuousTestsConfig.DeviceModule)
+_sym_db.RegisterMessage(ContinuousTestsConfig.DeviceModulesEntry)
+_sym_db.RegisterMessage(ContinuousTestsConfig.CipdRef)
+_sym_db.RegisterMessage(ContinuousTestsConfig.CipdRefs)
+_sym_db.RegisterMessage(ContinuousTestsConfig.CipdRefsEntry)
+
+
+_CONTINUOUSTESTSCONFIG_BUILDRESTRICTIONSENTRY._options = None
+_CONTINUOUSTESTSCONFIG_DEVICEMODULESENTRY._options = None
+_CONTINUOUSTESTSCONFIG_CIPDREFSENTRY._options = None
+# @@protoc_insertion_point(module_scope)
diff --git a/catatester/genfiles/master_config_pb2.py b/catatester/genfiles/master_config_pb2.py
new file mode 100644
index 0000000..eca5a3f
--- /dev/null
+++ b/catatester/genfiles/master_config_pb2.py
@@ -0,0 +1,429 @@
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: master_config.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='master_config.proto',
+  package='',
+  syntax='proto2',
+  serialized_pb=_b('\n\x13master_config.proto\"\xae\x01\n\nMasterInfo\x12\x10\n\x08hostname\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x19\n\nproduction\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x15\n\x07\x65nabled\x18\x04 \x01(\x08:\x04true\x12\x13\n\x0bmultimaster\x18\x05 \x01(\x08\x12\x0e\n\x06\x64\x62_url\x18\x06 \x01(\t\x12\x14\n\x0cproject_name\x18\x07 \x01(\t\x12\x13\n\x0blogger_name\x18\x08 \x01(\t\"F\n\x0fMasterScheduler\x12\x1c\n\x07\x64\x65tails\x18\x01 \x01(\x0b\x32\x0b.MasterInfo\x12\x15\n\rpoller_set_id\x18\x02 \x03(\t\"-\n\rMasterBuilder\x12\x1c\n\x07\x64\x65tails\x18\x01 \x01(\x0b\x32\x0b.MasterInfo\"^\n\x06Poller\x12\x14\n\x0cmanifest_url\x18\x01 \x01(\t\x12\x1f\n\x12poll_interval_secs\x18\x02 \x01(\x05:\x03\x36\x30\x30\x12\x1d\n\x06\x62ranch\x18\x03 \x03(\x0b\x32\r.PollerBranch\"\x7f\n\x0cPollerBranch\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x10\n\x08manifest\x18\x02 \x01(\t\x12\x1b\n\x13\x66ull_product_target\x18\x03 \x03(\t\x12\x1c\n\x14\x63ontent_shell_target\x18\x04 \x03(\t\x12\x14\n\x0c\x62uild_target\x18\x05 \x03(\t\"0\n\tPollerSet\x12\n\n\x02id\x18\x01 \x01(\t\x12\x17\n\x06poller\x18\x02 \x03(\x0b\x32\x07.Poller\"t\n\x0cMasterConfig\x12\x1f\n\x07\x62uilder\x18\x01 \x03(\x0b\x32\x0e.MasterBuilder\x12#\n\tscheduler\x18\x02 \x03(\x0b\x32\x10.MasterScheduler\x12\x1e\n\npoller_set\x18\x03 \x03(\x0b\x32\n.PollerSet')
+)
+
+
+
+
+_MASTERINFO = _descriptor.Descriptor(
+  name='MasterInfo',
+  full_name='MasterInfo',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='hostname', full_name='MasterInfo.hostname', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='name', full_name='MasterInfo.name', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='production', full_name='MasterInfo.production', index=2,
+      number=3, type=8, cpp_type=7, label=1,
+      has_default_value=True, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='enabled', full_name='MasterInfo.enabled', index=3,
+      number=4, type=8, cpp_type=7, label=1,
+      has_default_value=True, default_value=True,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='multimaster', full_name='MasterInfo.multimaster', index=4,
+      number=5, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='db_url', full_name='MasterInfo.db_url', index=5,
+      number=6, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='project_name', full_name='MasterInfo.project_name', index=6,
+      number=7, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='logger_name', full_name='MasterInfo.logger_name', index=7,
+      number=8, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=24,
+  serialized_end=198,
+)
+
+
+_MASTERSCHEDULER = _descriptor.Descriptor(
+  name='MasterScheduler',
+  full_name='MasterScheduler',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='details', full_name='MasterScheduler.details', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='poller_set_id', full_name='MasterScheduler.poller_set_id', index=1,
+      number=2, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=200,
+  serialized_end=270,
+)
+
+
+_MASTERBUILDER = _descriptor.Descriptor(
+  name='MasterBuilder',
+  full_name='MasterBuilder',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='details', full_name='MasterBuilder.details', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=272,
+  serialized_end=317,
+)
+
+
+_POLLER = _descriptor.Descriptor(
+  name='Poller',
+  full_name='Poller',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='manifest_url', full_name='Poller.manifest_url', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='poll_interval_secs', full_name='Poller.poll_interval_secs', index=1,
+      number=2, type=5, cpp_type=1, label=1,
+      has_default_value=True, default_value=600,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='branch', full_name='Poller.branch', index=2,
+      number=3, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=319,
+  serialized_end=413,
+)
+
+
+_POLLERBRANCH = _descriptor.Descriptor(
+  name='PollerBranch',
+  full_name='PollerBranch',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='name', full_name='PollerBranch.name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='manifest', full_name='PollerBranch.manifest', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='full_product_target', full_name='PollerBranch.full_product_target', index=2,
+      number=3, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='content_shell_target', full_name='PollerBranch.content_shell_target', index=3,
+      number=4, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='build_target', full_name='PollerBranch.build_target', index=4,
+      number=5, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=415,
+  serialized_end=542,
+)
+
+
+_POLLERSET = _descriptor.Descriptor(
+  name='PollerSet',
+  full_name='PollerSet',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='id', full_name='PollerSet.id', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='poller', full_name='PollerSet.poller', index=1,
+      number=2, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=544,
+  serialized_end=592,
+)
+
+
+_MASTERCONFIG = _descriptor.Descriptor(
+  name='MasterConfig',
+  full_name='MasterConfig',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='builder', full_name='MasterConfig.builder', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='scheduler', full_name='MasterConfig.scheduler', index=1,
+      number=2, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='poller_set', full_name='MasterConfig.poller_set', index=2,
+      number=3, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=594,
+  serialized_end=710,
+)
+
+_MASTERSCHEDULER.fields_by_name['details'].message_type = _MASTERINFO
+_MASTERBUILDER.fields_by_name['details'].message_type = _MASTERINFO
+_POLLER.fields_by_name['branch'].message_type = _POLLERBRANCH
+_POLLERSET.fields_by_name['poller'].message_type = _POLLER
+_MASTERCONFIG.fields_by_name['builder'].message_type = _MASTERBUILDER
+_MASTERCONFIG.fields_by_name['scheduler'].message_type = _MASTERSCHEDULER
+_MASTERCONFIG.fields_by_name['poller_set'].message_type = _POLLERSET
+DESCRIPTOR.message_types_by_name['MasterInfo'] = _MASTERINFO
+DESCRIPTOR.message_types_by_name['MasterScheduler'] = _MASTERSCHEDULER
+DESCRIPTOR.message_types_by_name['MasterBuilder'] = _MASTERBUILDER
+DESCRIPTOR.message_types_by_name['Poller'] = _POLLER
+DESCRIPTOR.message_types_by_name['PollerBranch'] = _POLLERBRANCH
+DESCRIPTOR.message_types_by_name['PollerSet'] = _POLLERSET
+DESCRIPTOR.message_types_by_name['MasterConfig'] = _MASTERCONFIG
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+MasterInfo = _reflection.GeneratedProtocolMessageType('MasterInfo', (_message.Message,), dict(
+  DESCRIPTOR = _MASTERINFO,
+  __module__ = 'master_config_pb2'
+  # @@protoc_insertion_point(class_scope:MasterInfo)
+  ))
+_sym_db.RegisterMessage(MasterInfo)
+
+MasterScheduler = _reflection.GeneratedProtocolMessageType('MasterScheduler', (_message.Message,), dict(
+  DESCRIPTOR = _MASTERSCHEDULER,
+  __module__ = 'master_config_pb2'
+  # @@protoc_insertion_point(class_scope:MasterScheduler)
+  ))
+_sym_db.RegisterMessage(MasterScheduler)
+
+MasterBuilder = _reflection.GeneratedProtocolMessageType('MasterBuilder', (_message.Message,), dict(
+  DESCRIPTOR = _MASTERBUILDER,
+  __module__ = 'master_config_pb2'
+  # @@protoc_insertion_point(class_scope:MasterBuilder)
+  ))
+_sym_db.RegisterMessage(MasterBuilder)
+
+Poller = _reflection.GeneratedProtocolMessageType('Poller', (_message.Message,), dict(
+  DESCRIPTOR = _POLLER,
+  __module__ = 'master_config_pb2'
+  # @@protoc_insertion_point(class_scope:Poller)
+  ))
+_sym_db.RegisterMessage(Poller)
+
+PollerBranch = _reflection.GeneratedProtocolMessageType('PollerBranch', (_message.Message,), dict(
+  DESCRIPTOR = _POLLERBRANCH,
+  __module__ = 'master_config_pb2'
+  # @@protoc_insertion_point(class_scope:PollerBranch)
+  ))
+_sym_db.RegisterMessage(PollerBranch)
+
+PollerSet = _reflection.GeneratedProtocolMessageType('PollerSet', (_message.Message,), dict(
+  DESCRIPTOR = _POLLERSET,
+  __module__ = 'master_config_pb2'
+  # @@protoc_insertion_point(class_scope:PollerSet)
+  ))
+_sym_db.RegisterMessage(PollerSet)
+
+MasterConfig = _reflection.GeneratedProtocolMessageType('MasterConfig', (_message.Message,), dict(
+  DESCRIPTOR = _MASTERCONFIG,
+  __module__ = 'master_config_pb2'
+  # @@protoc_insertion_point(class_scope:MasterConfig)
+  ))
+_sym_db.RegisterMessage(MasterConfig)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/catatester/proto/continuous_tests.proto b/catatester/proto/continuous_tests.proto
new file mode 100644
index 0000000..5475972
--- /dev/null
+++ b/catatester/proto/continuous_tests.proto
@@ -0,0 +1,201 @@
+syntax = "proto2";
+
+package continuous_tests;
+
+// When building proto outside of google3, it fails to build if any
+// enum values in the same message have the same name, because
+// natively enum values in a message are all siblings instead of being
+// local-scoped within their parent enum. As a result, we cannot
+// follow the google3 best practice to have the 0th enum value by DEFAULT
+// in all cases. These protos get copied to google3 via copybara, and in
+// google3 we get linter warnings when any enum values have the same name.
+// (-- api-linter: enum_default_value=disabled --)
+
+message BuildTargets {
+  enum BranchType {
+    ALL = 0;
+    MASTER = 1;
+    RELEASE = 2;
+  }
+  // Branch can be specified either by name or type.
+  message Branch {
+    oneof BranchSpec {
+      BranchType type = 1;
+      string name = 2;
+    }
+  }
+  enum Variant {
+    DEFAULT = 0;  // all variants
+    ENG = 1;
+    USER = 2;
+    ARM = 3;
+    X86 = 4;
+    X64 = 5;
+    ARM64 = 6;
+    USERDEBUG = 7;
+  }
+  // Branch types under test (default = All branch types).
+  optional BranchType branch_types = 1;
+  // Strings of product capabilities for the types of products under test.
+  // Product capabilities are defined in:
+  // https://cs.corp.google.com/eureka_internal/builder/masters/configs/eureka/build_config.protoascii
+  repeated string product_types = 2;
+  // Specific product(s) under test. Prefer using product_types where possible.
+  repeated string products = 3;
+  // Variants under test.
+  optional Variant variants = 4;
+  // (Optional) The builder_name builds being targeted.
+  // When unused (default), catatester will target all builder_names that
+  // match every specified <product>-<variant> combination.
+  // Set this to explicitly set builder_names for targeting non-conventional
+  // builds: i.e. steak-yeti, biggie-mesh.
+  // This is additive to any branch_types specified.
+  repeated string builder_names = 5;
+  // Branches under test (default = All branches).
+  repeated Branch branches = 6;
+}
+
+message Schedule {
+  // Cron-based scheduler string for how often to test new builds.
+  optional string cron = 1;
+  enum Policy {
+    DEFAULT = 0;           // by default, LATEST_UNTESTED_BUILD
+    LATEST_UNTESTED = 1;   // will backfill older untested builds
+    LATEST = 2;            // will test latest even if already tested before
+    LATEST_MINUS_ONE = 3;  // will test latest minus one untested OTA
+  }
+  // Scheduler policy for which build to test.
+  optional Policy policy = 2;
+}
+
+message BotRequirement {
+  optional string bot_dimension = 1;
+  optional string value = 2;
+}
+
+message CastOsModule {
+  optional string cipd_package = 1;
+  optional string cipd_version = 2;
+}
+
+message TestTarget {
+  enum Recipe {
+    HOME_RUNNER = 0;
+    DEVICE_UNIT_TEST_RUNNER = 1;
+    OTA_INSTALLABLE_RUNNER = 2;
+    LIBASSISTANT_RUNNER = 3;
+    FUCHSIA_GUNIT_RUNNER = 4;  // Deprecated
+    FUCHSIA_RUNNER = 5;
+    VIRTUAL_DEVICE_RUNNER = 6;
+  }
+  enum Priority {
+    DEFAULT = 0;  // Controller currently interprets default == medium
+    CRITICAL = 1;
+    MEDIUM = 2;
+    HIGH = 3;
+    LOW = 4;
+  }
+  // Path to the test config file. (relative to test_configs_root).
+  optional string config = 1;
+  // By default, catatester will run all test cases in the test config.
+  // Set this if you wish to run only a specific subset of named test cases that
+  // are defined in the test config file.
+  repeated string test_cases = 2;
+  // Name of the binary to execute for this test config.
+  optional string binary_name = 3;
+  // Test priority.
+  optional Priority priority = 4;
+  // Whether this test target is enabled (default = true).
+  optional bool enabled = 5 [default = true];
+  // The luci-py recipe to use for running the swarming task.
+  optional Recipe recipe = 6;
+  // The test timeout (mins).  Does not include ota install time.
+  optional int32 test_timeout_mins = 7 [default = 60];
+  // Names of test groups that this test belongs to.
+  // Test group tags must be defined in test_group_tags.
+  repeated string test_groups = 8;
+  // Optional list of testbed requirements that the test requires.
+  repeated BotRequirement testbed_requirements = 9;
+  // the key of the cipd_refs map to be used for this test.
+  optional string cipd_refs_key = 10 [default = 'internal'];
+  // Bot pool to use for allocation. Default value set by controller.
+  optional string bot_pool = 11;
+  // CastOs Modules to install before running the test.
+  repeated CastOsModule castos_modules = 12;
+  // Whether to skip setting default bot dimensions when scheduling the test.
+  // If True, all dimensions (other than pool) must be set in
+  // testbed_requirements.
+  optional bool skip_default_dimensions = 13;
+}
+
+enum BuildPlatform {
+  DEFAULT = 0;
+  CATABUILDER = 1;
+  ANDROID_BUILD = 2;
+}
+
+message ContinuousTest {
+  optional TestTarget test_target = 1;
+  repeated BuildTargets build_targets = 2;
+}
+
+message ScheduledTest {
+  optional TestTarget test_target = 1;
+  repeated BuildTargets build_targets = 2;
+  // Set if you want a cron-based scheduler for running tests.
+  // When not set (by default), catatester will test every matching new build
+  // continuously.
+  optional Schedule schedule = 3;
+  repeated string branches = 4;
+  optional string repo = 5 [default = "internal"];
+  // (Optional) The platform where the build comes from.
+  // By default, every build is from Catabuilder.
+  optional BuildPlatform build_platform = 6 [default = CATABUILDER];
+}
+
+message ContinuousTestsConfig {
+  // Relative path from continuous-tests repo to the test configs root dir.
+  optional string test_configs_root = 1;
+
+  // Continuous test targets.
+  repeated ContinuousTest continuous_test = 2;
+
+  // Scheduled test targets.
+  repeated ScheduledTest scheduled_test = 3;
+
+  // Priority-Ordering of branch types.
+  repeated BuildTargets.BranchType branch_type_priorities = 4;
+  // Branch names that we no longer want to test.
+  repeated string disabled_branches = 5;
+
+  // Build restrictions for certain products.
+  // Useful for new products where we don't have enough inventory to test every
+  // build_target permutation desired by the tests.
+  message BuildRestrictions {
+    repeated BuildTargets.BranchType allowed_branch_types = 1;
+    repeated BuildTargets.Variant allowed_variants = 2;
+  }
+  map<string, BuildRestrictions> build_restrictions = 6;
+
+  // Defines common test group tags.
+  repeated string test_group_tags = 7;
+
+  // Device modules passed to the test runner to instantiate the correct
+  // device class during the execution of tests.
+  message DeviceModule {
+    optional string device_module = 1;
+    optional string device_class = 2;
+  }
+  map<string, DeviceModule> device_modules = 8;
+
+  // Per repo CIPD references
+  message CipdRef {
+    optional string package_name = 1;
+    optional string version = 2;
+    optional string path = 3;
+  }
+  message CipdRefs {
+    repeated CipdRef refs = 1;
+  }
+  map<string, CipdRefs> cipd_refs = 9;
+}
diff --git a/catatester/proto/copy.bara.sky b/catatester/proto/copy.bara.sky
new file mode 100644
index 0000000..14a33cf
--- /dev/null
+++ b/catatester/proto/copy.bara.sky
@@ -0,0 +1,51 @@
+"""Copybara config to auto import build_config protos to continuous-tests.
+
+Usage:
+  alias copybara='/google/data/ro/teams/copybara/copybara'
+  copybara catatester/proto/copy.bara.sky build_configs_import
+"""
+
+git_origin = git.origin(
+    url = "https://eureka-internal.googlesource.com/builder/masters",
+    ref = "master",
+)
+
+origin_files = glob(
+    include = [
+        "src/proto/build_config.proto",
+        "src/genfiles/build_config_pb2.py",
+        "configs/eureka/build_config.protoascii"
+    ],
+    exclude = [],
+)
+
+destination_files = glob(
+    include = [
+        "catatester/proto/imported/build_config.proto",
+        "catatester/genfiles/build_config_pb2.py",
+        "catatester/configs/imported/build_config.protoascii"],
+    exclude = [],
+)
+
+core.workflow(
+    name = "build_configs_import",
+    origin = git_origin,
+    origin_files = origin_files,
+    destination = git.gerrit_destination(
+        url = "https://eureka-internal.googlesource.com/continuous-tests",
+        fetch = "master",
+        push_to_refs_for = "master",
+    ),
+    destination_files = destination_files,
+    authoring = authoring.pass_thru("Home EngProd <no-reply@google.com>"),
+    transformations = [
+        core.move("src/proto/build_config.proto", "catatester/proto/imported/build_config.proto"),
+        core.move("src/genfiles/build_config_pb2.py", "catatester/genfiles/build_config_pb2.py"),
+        core.move(
+            "configs/eureka/build_config.protoascii",
+            "catatester/configs/imported/build_config.protoascii"),
+        metadata.squash_notes(
+            prefix = "Imports from eureka_internal:builder/masters.\n\n",
+        ),
+    ],
+)
diff --git a/catatester/proto/imported/build_config.proto b/catatester/proto/imported/build_config.proto
new file mode 100644
index 0000000..2af9d83
--- /dev/null
+++ b/catatester/proto/imported/build_config.proto
@@ -0,0 +1,36 @@
+syntax = "proto2";
+
+package build_config;
+
+// Disable linter for copybara exports to google3
+// [NOLINT common_typos_disable]
+
+message OtaBuild {
+  // The meta build target to build for. For example steak-eng.
+  optional string build_target = 1;
+  // The product to build for. This is something like steak or anchovy. Note
+  // that on newer platforms this is the same as the platform.
+  optional string product = 2;
+  // The variant this is for. For example user or eng.
+  optional string variant = 3;
+  // The platform this is for.
+  optional string platform = 4;
+  // A list of partner buckets to archive artifacts to.
+  repeated string partner_bucket = 5;
+  // A list of capabilities that this product has.
+  enum Capability {
+    NONE = 0;
+    ASSISTANT = 1;
+    CAST_VIDEO_RECEIVER = 2;
+    CAST_AUDIO_RECEIVER = 3;
+    WIFI_2GHZ = 4;
+    WIFI_5GHZ = 5;
+  }
+  repeated Capability capabilities = 6;
+  // Whether or not this build is enabled for automated tests
+  optional bool test = 7 [default = false];
+}
+
+message BuildConfig {
+  map<string, OtaBuild> ota_build = 1;
+}
diff --git a/catatester/puppet/README.md b/catatester/puppet/README.md
new file mode 100644
index 0000000..c712ab0
--- /dev/null
+++ b/catatester/puppet/README.md
@@ -0,0 +1,2 @@
+# DO NOT modify the puppet configuration files
+The configuration (.pp) files are copied from google3 via copybara service.
diff --git a/catatester/puppet/cast_builder/files/swarm/lab_nodes/site.pp b/catatester/puppet/cast_builder/files/swarm/lab_nodes/site.pp
new file mode 100644
index 0000000..01273d6
--- /dev/null
+++ b/catatester/puppet/cast_builder/files/swarm/lab_nodes/site.pp
@@ -0,0 +1,58 @@
+# Node definitions for swarm lab hosts
+#
+# See go/catatester-hosts for host locations & static IP addresses
+
+$md5_cipd_key = 'f075d7d239f62829a2da23276990efbf'
+$md5_gsutil_boto = 'defa9c638098738683e410a04320d8db'
+$md5_fragglerock_key = '2c52eb278c88fdb7c993880fa2910109'
+$md5_monitoring_key = '4886a0a7112098e2de658251269b30fe'
+$md5_swarming_key = '1c997a30801f9b7b82dcb537227f7712'
+
+node /^home-staging-.*/ {
+  $cipd_env = 'staging'
+  class {'cast_builder::swarm_init':
+    env => 'lab_host',
+  }
+}
+node /^home-swarming-.*/ {
+  $cipd_env = 'live'
+  class {'cast_builder::swarm_init':
+    env  => 'lab_host',
+  }
+}
+node /^home-cq-.*/ {
+  $cipd_env = 'live'
+  class {'cast_builder::swarm_init':
+    env  => 'lab_host',
+  }
+}
+node /^gotham-1$/ {
+  $cipd_env = 'live'
+  class {'cast_builder::swarm_init':
+    env => 'gotham_host',
+  }
+}
+node /^gazoo-canary-.*/ {
+  $cipd_env = 'canary'
+  class {'cast_builder::swarm_init':
+    env => 'gazoo_canary_host',
+  }
+}
+node /^gazoo-staging-.*/ {
+  $cipd_env = 'staging'
+  class {'cast_builder::swarm_init':
+    env => 'gazoo_staging_host',
+  }
+}
+node /^gazoo-prod-.*/ {
+  $cipd_env = 'live'
+  class {'cast_builder::swarm_init':
+    env => 'gazoo_prod_host',
+  }
+}
+node default {
+  $cipd_env = 'live'
+  class {'cast_builder::swarm_init':
+    env => 'lab_host',
+  }
+}
diff --git a/catatester/puppet/cast_builder/lib/facter/devices.rb b/catatester/puppet/cast_builder/lib/facter/devices.rb
new file mode 100644
index 0000000..6e0934e
--- /dev/null
+++ b/catatester/puppet/cast_builder/lib/facter/devices.rb
@@ -0,0 +1,17 @@
+def ls(dir)
+  if File.directory?(dir)
+    Dir.entries(dir).reject { |entry| entry == '.' || entry == '..' }
+  else
+    Array.new
+  end
+end
+
+Facter.add('usb_devices') do
+  confine :kernel => 'Linux'
+
+  # udev rules configure the system to generate symlinks in this directory
+  # when devices are connected to the machine (see: 60-persistent-serial.rules)
+  setcode do
+    ls('/dev/serial/by-path/')
+  end
+end
diff --git a/catatester/puppet/cast_builder/manifests/swarm_bot_gotham.pp b/catatester/puppet/cast_builder/manifests/swarm_bot_gotham.pp
new file mode 100644
index 0000000..a1e0ff5
--- /dev/null
+++ b/catatester/puppet/cast_builder/manifests/swarm_bot_gotham.pp
@@ -0,0 +1,124 @@
+# Setup for swarm bots in Gotham CI.
+# Enumerates the connected Gothams, and bootstraps a Swarming bot for each.
+# Uses Systemd to restart bots in case they or the host crashes.
+
+class cast_builder::swarm_bot_gotham ($user, $group, $home_dir) {
+
+  $cipd_key        = "${home_dir}/home-cipd-service-account.json"
+  $gsutil_dir      = "${home_dir}/gsutil"
+  $gsutil_boto     = "${home_dir}/.boto"
+  $fragglerock_key = "${home_dir}/fragglerock_service_key.json"
+  $swarm_home_dir  = "${home_dir}/swarm_home"
+  $depot_tools_dir = "${home_dir}/depot_tools"
+  $swarm_bots_home = "${swarm_home_dir}/bots"
+  $venv            = "${swarm_home_dir}/swarm_env"
+  $prefix          = 'swarming_bot--'
+
+  $packages = [
+    'libudev-dev',
+    'libusb-1.0.0-dev',
+    'virtualenv',
+  ]
+
+  package { $packages:
+    ensure  => latest,
+  }
+
+  file { '/etc/udev/rules.d/99-nxpusb.rules':
+    ensure  => file,
+    owner   => $user,
+    group   => $group,
+    mode    => '0644',
+    content => '
+# Give non-superusers access to connected NXP boards
+
+# SDP protocol
+KERNEL=="hidraw*", ATTRS{idVendor}=="1fc9", MODE="0666"
+ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="1fc9", MODE="666"
+ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="0d28", MODE="666"
+
+# Flashloader
+KERNEL=="hidraw*", ATTRS{idVendor}=="15a2", MODE="0666"'
+  }
+
+  file { '/etc/udev/rules.d/70-caraway.rules':
+    ensure  => file,
+    owner   => $user,
+    group   => $group,
+    mode    => '0644',
+    content => '
+# 0483:374b STMicroelectronics ST-LINK/V2.1 (Nucleo-F103RB)
+# This grants rw to plugdev so that the usb device is accessible to tools (e.g.
+# openocd) without sudo.
+SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="374b", MODE="0664", GROUP="plugdev", SYMLINK+="stlink-latest", SYMLINK+="stlink%n", RUN+="/usr/bin/touch /tmp/A"
+
+# 0483:df11 STMicroelectronics STM Device in DFU Mode
+# This grants rw to plugdev so that the usb device is accessible to tools (e.g. dfuse.py) without sudo.
+SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="df11", MODE="0664", GROUP="plugdev"
+
+# 18d1:* Google Inc.
+# Prevents modem-manager from probing for a modem on any serial ports with
+# idVendor == Google, and grants rw to plugdev so that the usb device is
+# accessible to tools without sudo.
+SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ENV{ID_MM_DEVICE_IGNORE}="1", MODE="0664", GROUP="plugdev"
+
+# If you need to be more specific than just using idVendor == Google, i.e. you are also using
+# separate rules for other Google devices, you can comment out the rule directly above this
+# and uncomment the following rule, filling in the appropriate value for idProduct. Note the
+# use of the plural "ATTRS" to match parents of the device being added.
+# SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTRS{idProduct}=="", ENV{ID_MM_DEVICE_IGNORE}="1", MODE="0664", GROUP="plugdev"'
+  }
+
+  # usb_devices is a Facter fact enumerating the connected Gothams.
+  # This is invoked at runtime and defined in lib/facter/devices.rb.
+  $::usb_devices.each |String $serial_port| {
+    $bot_id = sha1($serial_port)[0,6]
+
+    file { [ "${swarm_bots_home}/${bot_id}/",
+             "${swarm_bots_home}/${bot_id}/bot_root/", ]:
+      ensure => directory,
+      owner  => $user,
+      group  => $group,
+      mode   => '0755',
+    } ->
+    file { "${swarm_bots_home}/${bot_id}/bot_dimensions.json":
+      ensure  => file,
+      owner   => $user,
+      group   => $group,
+      mode    => '0644',
+      content => "{ \"gotham_serial_port\" : [\"${serial_port}\"] }",
+    } ->
+    # Note that bots routinely clear the directory in which they were started.
+    # Any file or directory other than swarming_bot.zip will be deleted.
+    file { "${swarm_bots_home}/${bot_id}/bot_root/swarming_bot.zip":
+      ensure => file,
+      owner  => $user,
+      group  => $group,
+      mode   => '0755',
+
+      # TODO(dustingadal) This is curled here by the host bootstrap script.
+      # It shouldn't be placed in the bot-specific directory bots/1/
+      # See: //eureka_internal/test/lab_system/setup_swarm_host.sh
+      source => "file:${swarm_bots_home}/1/swarming_bot.zip",
+    } ->
+    # This will restart the bot if it or the host goes down.
+    file { "/etc/systemd/system/${prefix}${bot_id}.service":
+      ensure  => file,
+      owner   => 'root',
+      group   => 'root',
+      mode    => '0644',
+      content => template('cast_builder/swarm_bot.systemd.erb'),
+    } ->
+    service { "${prefix}${bot_id}":
+      ensure   => running,
+      enable   => true,
+      provider => 'systemd',
+      before   => Exec['restart_systemd'],
+    }
+  }
+
+  # The services previously registered are run as systemd restarts.
+  exec { 'restart_systemd':
+    command => '/bin/systemctl daemon-reload',
+  }
+}
diff --git a/catatester/puppet/cast_builder/manifests/swarm_bot_home.pp b/catatester/puppet/cast_builder/manifests/swarm_bot_home.pp
new file mode 100644
index 0000000..b151dc4
--- /dev/null
+++ b/catatester/puppet/cast_builder/manifests/swarm_bot_home.pp
@@ -0,0 +1,125 @@
+# Setup for swarm bots in Home CI
+
+class cast_builder::swarm_bot_home ($user, $group, $home_dir) {
+
+  # Save globals as in-scope class variables
+  $cipd_ref         = $cipd_env  # lint:ignore:variable_scope
+
+  # Define class variables
+  $cipd_key                        = "${home_dir}/home-cipd-service-account.json"
+  $gsutil_dir                      = "${home_dir}/gsutil"
+  $gsutil_boto                     = "${home_dir}/.boto"
+  $fragglerock_key                 = "${home_dir}/fragglerock_service_key.json"
+  $swarm_home_dir                  = "${home_dir}/swarm_home"
+  $depot_tools_dir                 = "${home_dir}/depot_tools"
+  $swarm_bots_home                 = "${swarm_home_dir}/bots"
+  $eurtest_binaries                = "${swarm_home_dir}/eurtest_binaries"
+  $cipd_ensure_eurtest_binaries_sh = "${home_dir}/cipd_ensure_eurtest_binaries.sh"
+  $cipd_ensure_eurtest_binaries    = "${home_dir}/cipd_ensure_eurtest_binaries"
+  $devicemap_dir                   = "${swarm_home_dir}/devicemap"
+  $cipd_ensure_devicemap           = "${home_dir}/cipd_ensure_devicemap"
+  $venv                            = "${swarm_home_dir}/swarm_env"
+  $good_builds_dir                 = "${swarm_home_dir}/good_builds"
+  $androidthings_keys_dir          = "${swarm_home_dir}/androidthings_keys"
+
+  ### Ensure packages retrieved via cipd
+
+  File {
+    ensure => file,
+    owner  => $user,
+    group  => $group,
+    mode   => '0750',
+  }
+  Exec {
+    user  => $user,
+  }
+
+  file { '/tmp/ensure_androidthings_keys':
+    ensure  => file,
+    content => "home_internal/androidthings_keys ${cipd_ref}",
+  } ->
+  exec { 'cipd_ensure_androidthings_keys':
+    command => "${depot_tools_dir}/cipd ensure -ensure-file /tmp/ensure_androidthings_keys -root ${androidthings_keys_dir} -service-account-json ${cipd_key}",
+    require => Notify['has_cipd_key'],
+    timeout => 2700,
+    before  => Exec['setup_bots'],
+  }
+
+  file { '/tmp/ensure_good_builds':
+    ensure  => file,
+    content => "home_internal/known_good_builds ${cipd_ref}",
+  } ->
+  exec { 'cipd_ensure_good_builds':
+    command => "${depot_tools_dir}/cipd ensure -ensure-file /tmp/ensure_good_builds -root ${good_builds_dir} -service-account-json ${cipd_key}",
+    require => Notify['has_cipd_key'],
+    timeout => 2700,
+    before  => Exec['setup_bots'],
+  }
+
+  file { $cipd_ensure_eurtest_binaries:
+    ensure  => file,
+    content => template('cast_builder/ensure_eurtest_binaries.erb'),
+  } ->
+  file { $cipd_ensure_eurtest_binaries_sh:
+    ensure  => file,
+    content => template('cast_builder/ensure_eurtest_binaries.sh.erb'),
+  } ->
+  # Ensure we always have a cron that downloads latest test binaries, which
+  # will always d/l even if currently-used puppet scripts are broken.
+  cron { 'cron_cipd_ensure_eurtest_binaries':
+    ensure      => present,
+    command     => "${cipd_ensure_eurtest_binaries_sh} 2>&1 | /usr/bin/logger -t ensure_eurtest_binaries",
+    minute      => 0,
+    environment => [
+      'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
+    ],
+    user        => $user,
+    require     => Notify['has_cipd_key'],
+    before      => Exec['setup_bots'],
+  } ->
+  # Directly execute once, for first-time setup. This must happen before setup_bots.
+  exec { 'cipd_ensure_eurtest_binaries':
+    command => "${cipd_ensure_eurtest_binaries_sh} 2>&1 | /usr/bin/logger -t ensure_eurtest_binaries",
+    require => Notify['has_cipd_key'],
+    timeout => 3000,
+    before  => Exec['setup_bots'],
+  }
+
+  # Ensure we always have a cron that downloads latest devicemap scripts, which
+  # will always d/l even if currently-used puppet scripts are broken.
+  file { $cipd_ensure_devicemap:
+    content => "home_internal/devicemap ${cipd_ref}",
+  } ->
+  cron { 'cron_cipd_ensure_devicemap':
+    ensure      => present,
+    command     => "${depot_tools_dir}/cipd ensure -ensure-file ${cipd_ensure_devicemap} -root ${devicemap_dir} -service-account-json ${cipd_key} 2>&1 | /usr/bin/logger -t ensure_devicemap",
+    minute      => '*/10',  # every 10 mins
+    environment => [
+      'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
+    ],
+    user        => $user,
+    before      => Exec['setup_bots'],
+    require     => Notify['has_cipd_key'],
+  } ->
+  # Directly execute once, for first-time setup. This must happen before setup_bots.
+  exec { 'cipd_ensure_devicemap':
+    command => "${depot_tools_dir}/cipd ensure -ensure-file ${cipd_ensure_devicemap} -root ${devicemap_dir} -service-account-json ${cipd_key} 2>&1 | /usr/bin/logger -t ensure_devicemap",
+    require => Notify['has_cipd_key'],
+    timeout => 120,
+    before  => Exec['setup_bots'],
+  }
+
+  exec { 'setup_bots':
+    # TODO(jonesmi): fetch swarming token using _ahi/api/v1/swarming_token using gcloud APIs
+    cwd     => $home_dir,
+    path    => '/bin',
+    command => "bash -c 'source ${venv}/bin/activate && python ${devicemap_dir}/bot_setup.py --user ${user} --group ${group} --swarm_bots_home ${swarm_bots_home} --gsutil_dir ${gsutil_dir} --gsutil_boto ${gsutil_boto} --fragglerock_key ${fragglerock_key} --eurtest_binaries ${eurtest_binaries} --venv ${venv} --devicemap_dir ${devicemap_dir} --depot_tools_dir ${depot_tools_dir}'",
+    user    => root,
+    require => [
+      File[$swarm_bots_home],
+      Exec['cipd_ensure_eurtest_binaries',
+           'pip_install_requirements',
+           'install gsutil']]
+  }
+  # TODO(jonesmi): also add a script to auto-stop & delete upstarts for bots that get deleted from $bots_list
+}
diff --git a/catatester/puppet/cast_builder/manifests/swarm_host_base.pp b/catatester/puppet/cast_builder/manifests/swarm_host_base.pp
new file mode 100644
index 0000000..0bf22a8
--- /dev/null
+++ b/catatester/puppet/cast_builder/manifests/swarm_host_base.pp
@@ -0,0 +1,202 @@
+# Common setup for all Swarming device-bot hosts for CI
+
+class cast_builder::swarm_host_base ($user, $group, $home_dir) {
+
+  # save globals as in-scope class variables
+  $cipd_ref           = $cipd_env  # lint:ignore:variable_scope
+
+  # define class variables
+  $cipd_key           = "${home_dir}/home-cipd-service-account.json"
+  $gsutil_dir         = "${home_dir}/gsutil"
+  $gsutil_boto        = "${home_dir}/.boto"
+  $fragglerock_key    = "${home_dir}/fragglerock_service_key.json"
+  $swarm_home_dir     = "${home_dir}/swarm_home"
+  $depot_tools_dir    = "${home_dir}/depot_tools"
+  $depot_tools_remote = 'https://chromium.googlesource.com/chromium/tools/depot_tools.git'
+  $swarm_bots_home    = "${swarm_home_dir}/bots"
+  $venv               = "${swarm_home_dir}/swarm_env"
+  $email              = 'livingroom-releng+alerts+puppet@google.com'
+
+  $common_packages = [
+      'alsa-utils',  # Provides "arecord" and "amixer", used by MZ sync tests.
+      'avahi-utils',  # Provides "avahi-browse", for MZ discovery tests.
+      'build-essential',
+      'curl',
+      'git',
+      'libcurl4-gnutls-dev',
+      'libffi-dev',
+      'libfreetype6-dev',
+      'libsox-fmt-mp3',
+      'lm-sensors',
+      'nodejs',
+      'python-dev',
+      'python-oauth2client',
+      'python-setuptools',
+      'python-virtualenv',
+      'sox',
+      'tar',
+      'vim',
+  ]
+
+  # For debian 10.0 libav-tools is replaced by ffmpeg
+  $major_os_release = 0.0 + $::operatingsystemmajrelease
+  if $major_os_release >= 10.0 {
+    $packages = $common_packages + ['ffmpeg']
+  }
+  else {
+    $packages = $common_packages + ['libav-tools']
+  }
+
+  package { $packages:
+    ensure  => present,
+    require => Exec['apt-get_update'],
+  }
+
+  Exec {
+    # git needs HOME to know where to write the global config.
+    environment => ["HOME=${home_dir}"],
+    path        => ['/usr/local/sbin', '/usr/local/bin', '/usr/sbin', '/usr/bin', '/sbin', '/bin', $depot_tools_dir],
+    user        => $user,
+  }
+
+  File {
+    owner => $user,
+    group => $group,
+  }
+
+  exec { 'apt-get_update':
+    user    => 'root',
+    group   => 'root',
+    command => '/usr/bin/apt-get update',
+    timeout => 300,
+  }
+
+  group { $group:
+      ensure => 'present',
+  }
+
+  file { $home_dir:
+    ensure => directory,
+    mode   => '0755'
+  }
+
+  file { $swarm_bots_home:
+    ensure => directory,
+    mode   => '0755'
+  }
+
+  file { $cipd_key:
+    ensure => file,
+    mode   => '0750'
+  } ->
+  cast_builder::test_md5 { $cipd_key:
+    md5 => $::md5_cipd_key,
+  } ->
+  notify { 'has_cipd_key':
+    message => "cipd file exists at ${cipd_key}",
+  }
+
+  exec { 'clone_depot_tools_repo':
+    cwd     => $home_dir,
+    command => "git clone ${depot_tools_remote}",
+    creates => $depot_tools_dir,
+    require => Package['git'],
+  } ->
+  exec { 'git_pull_depot_tools':
+    cwd     => $depot_tools_dir,
+    command => 'git pull origin master',
+  }
+
+  ### Pip & virtualenv setup
+
+  file { $venv:
+    ensure  => directory,
+    mode    => '0755',
+    require => File[$home_dir],
+  }
+
+  $pip_cmd = "${venv}/bin/pip"
+  exec { 'create_python_virtualenv':
+    command => "virtualenv ${venv}; ${pip_cmd} --log ${venv}/pip.log install --upgrade pip setuptools",
+    creates => "${venv}/bin/activate",
+    cwd     => $home_dir,
+    # Unless activate exists and VIRTUAL_ENV is correct we re-create the virtualenv
+    unless  => "grep '^[\\t ]*VIRTUAL_ENV=[\\\\'\\\"]*${venv}[\\\"\\\\'][\\t ]*$' ${venv}/bin/activate",
+    require => [
+        Package['python-virtualenv'],
+        File[$venv]
+    ],
+  } ->
+  # Enable $user to write to its own .cache directory.
+  file { "${home_dir}/.cache":
+    ensure => 'directory',
+    owner  => $user,
+    group  => $group,
+  } ->
+  exec { 'pip_install_requirements':
+    command => "bash -c 'source ${venv}/bin/activate && ${pip_cmd} install -U pip wheel &&
+                ${pip_cmd} install pyyaml'",
+    require => [
+        Exec['create_python_virtualenv'],
+        Package['python-virtualenv'],
+    ],
+    timeout => 120,
+  }
+
+  ### Ensure that gsutil is set up. Some tests use it to copy files to a media server.
+  # File with no source will be created with 0 size by puppet if missing
+  # Manual deployment of .boto and .p12 file required for new host
+  # (see instructions in swarm_init.pp)
+  file { $gsutil_boto:
+    ensure  => file,
+    mode    => '0755',
+    require => File[$home_dir],
+  } ->
+  cast_builder::test_md5 { $gsutil_boto:
+    md5 => $::md5_gsutil_boto,
+  } ->
+  notify { 'correct_boto':
+    message => "Successfully tested file ${home_dir}/.boto",
+  }
+
+  # File with no source will be created with 0 size by puppet if missing
+  # Manual deployment of json key file required for new host
+  # (see instructions in swarm_init.pp)
+  file { $fragglerock_key:
+    ensure  => file,
+    mode    => '0755',
+    require => File[$home_dir],
+  } ->
+  cast_builder::test_md5 { $fragglerock_key:
+    md5 => $::md5_fragglerock_key,
+  } ->
+  notify { 'correct_fragglerock_key_file':
+    message => "Successfully tested file ${fragglerock_key}",
+  }
+
+  # Get, install, update, and configure gsutil
+  file { $gsutil_dir:
+    ensure  => directory,
+    mode    => '0755',
+    require => Notify['correct_boto'],
+  } ->
+  exec { 'get gsutil':
+    cwd     => $home_dir,
+    command => 'curl https://storage.googleapis.com/pub/gsutil.tar.gz -f -o gsutil.tar.gz',
+    require => Package['curl'],
+  } ->
+  exec { 'install gsutil':
+    cwd     => $home_dir,
+    command => "tar --ungzip --extract --strip-components=2 --file=gsutil.tar.gz --directory=${gsutil_dir}",
+    require => Package['tar'],
+  } ->
+  file { "${home_dir}/gsutil.tar.gz":
+    ensure => absent,
+  # Disable gsutil update until a safe release is available. b/135462553
+  # } ->
+  # # gsutil will not run but return 0 status with empty .boto file
+  # exec { 'update gsutil':
+  #   cwd     => $home_dir,
+  #   command => "${gsutil_dir}/gsutil update -f -n",
+  }
+}
diff --git a/catatester/puppet/cast_builder/manifests/swarm_host_ganeti.pp b/catatester/puppet/cast_builder/manifests/swarm_host_ganeti.pp
new file mode 100644
index 0000000..479caae
--- /dev/null
+++ b/catatester/puppet/cast_builder/manifests/swarm_host_ganeti.pp
@@ -0,0 +1,32 @@
+# Setup Raspberry Pi swarm host in lab for Home CI
+
+class cast_builder::swarm_host_ganeti ($user, $home_dir) {
+
+  $email           = 'livingroom-releng+alerts+puppet@google.com'
+  $depot_tools_dir = "${home_dir}/depot_tools"
+
+  $packages = [
+      'become',
+  ]
+
+  package { $packages:
+    ensure  => latest,
+  }
+
+  apt::addon_repo { 'cast-builder-puppet':
+    repo_stage => $::cast_builder_track,
+  }
+
+  # Until Goobuntu Team makes puppet frequency configurable (b/22848425) we need
+  # to manually ensure that Puppet runs hourly on all hosts.
+  cron::entry { 'goobuntu_run_puppet':
+    ensure      => present,
+    command     => '/usr/bin/apt-get update; /usr/bin/glinux-run-puppet',
+    minute      => fqdn_rand(60),
+    user        => 'root',
+    environment => {
+      'PATH'   => '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
+      'MAILTO' => $email,
+    },
+  }
+}
diff --git a/catatester/puppet/cast_builder/manifests/swarm_host_lab.pp b/catatester/puppet/cast_builder/manifests/swarm_host_lab.pp
new file mode 100644
index 0000000..a176502
--- /dev/null
+++ b/catatester/puppet/cast_builder/manifests/swarm_host_lab.pp
@@ -0,0 +1,126 @@
+# Set up common components for Swarming hosts in the lab
+
+class cast_builder::swarm_host_lab ($user, $group, $home_dir) {
+
+  # save globals as in-scope class variables
+  $cipd_ref        = $cipd_env  # lint:ignore:variable_scope
+
+  $swarm_home_dir     = "${home_dir}/swarm_home"
+  $cipd_ensure_puppet = "${home_dir}/cipd_ensure_puppet"
+  $puppet_dir         = "${home_dir}/puppet"
+  $puppet_manifest    = "${puppet_dir}/cast_builder/files/swarm/lab_nodes/site.pp"
+  $puppet_run_sh      = "${home_dir}/swarm_puppet_run.sh"
+  $depot_tools_dir    = "${home_dir}/depot_tools"
+  $cipd_key           = "${home_dir}/home-cipd-service-account.json"
+  # TODO(b/79684886): better service account key management.
+  $monitoring_key     = "${home_dir}/home-ci-controller-staging.json"
+  $monitoring_tool    = "${swarm_home_dir}/eurtest_binaries/home_binaries/tools/host_monitor"
+  $email              = 'livingroom-releng+alerts+puppet@google.com'
+  $swarming_key       = "${home_dir}/luci/swarming-key.json"
+
+  $packages = [
+      'puppet',
+  ]
+
+  File {
+    ensure => file,
+    owner  => $user,
+    group  => $group,
+    mode   => '0755',
+  }
+
+  package { $packages:
+    ensure  => latest,
+  }
+
+  file { $monitoring_key:
+    require => File[$home_dir],
+  } ->
+  cast_builder::test_md5 { $monitoring_key:
+    md5 => $::md5_monitoring_key,
+  } ->
+  notify { 'correct_monitoring_key_file':
+    message => "Successfully tested file ${monitoring_key}",
+  }
+
+  file { $swarming_key:
+    require => File[$home_dir],
+  } ->
+  cast_builder::test_md5 { $swarming_key:
+    md5 => $::md5_swarming_key,
+  } ->
+  notify { 'correct_swarming_key_file':
+    message => "Successfully tested file ${monitoring_key}",
+  }
+
+
+  # Install ADB and Fastboot
+  exec { 'download_adb':
+    cwd     => $swarm_home_dir,
+    command => "/usr/bin/wget https://dl.google.com/android/repository/platform-tools-latest-linux.zip -O ${swarm_home_dir}/platform-tools-latest-linux.zip",
+  } ->
+  exec { 'extract_adb':
+    cwd     => $swarm_home_dir,
+    command => "/usr/bin/unzip -o -q ${swarm_home_dir}/platform-tools-latest-linux.zip -d ${swarm_home_dir}/",
+  } ->
+  file { '/usr/local/sbin/adb':
+    ensure   => file,
+    mode     => '0755',
+    source   => "${swarm_home_dir}/platform-tools/adb",
+    checksum => md5,
+    backup   => false
+  } ->
+  file { '/usr/local/sbin/fastboot':
+    ensure   => file,
+    mode     => '0755',
+    source   => "${swarm_home_dir}/platform-tools/fastboot",
+    checksum => md5
+  } ->
+  file { "${swarm_home_dir}/platform-tools-latest-linux.zip":
+    ensure  => absent,
+  } ->
+  file { "${swarm_home_dir}/platform-tools":
+    ensure  => absent,
+    recurse => true,
+    purge   => true,
+    force   => true,
+  }
+
+  # (TODO:kevinfei) remove deleted cron once change is rolled out 100%
+  file { $puppet_run_sh:
+    ensure  => file,
+    content => template('cast_builder/swarm_puppet_run.sh.erb'),
+  } ->
+  file { $cipd_ensure_puppet:
+    content => "home_internal/swarm_puppet ${cipd_ref}",
+  } ->
+  cron { 'cipd_ensure_puppet':
+    ensure      => absent,
+    # Ensure we always have a cron that downloads latest puppet scripts before
+    # actually executing them, so that hosts can self-update even if their
+    # current copy of puppet scripts are broken.
+    command     => "${depot_tools_dir}/cipd ensure -ensure-file ${cipd_ensure_puppet} -root ${puppet_dir} -service-account-json ${cipd_key}  && /usr/bin/apt-get update; /usr/bin/puppet apply --modulepath ${puppet_dir} ${puppet_manifest} --logdest syslog; ${monitoring_tool} --service_account_json ${monitoring_key} --puppet_run_exit_code \$?",
+    minute      => '*/30',  # every 30 mins
+    require     => [
+        Package['puppet'],
+    ],
+    environment => [
+      'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
+    ],
+  } ->
+  cron { 'cipd_ensure_puppet_monitoring':
+    ensure      => present,
+    # Ensure we always have a cron that downloads latest puppet scripts before
+    # actually executing them, so that hosts can self-update even if their
+    # current copy of puppet scripts are broken.
+    command     => "/bin/bash ${puppet_run_sh}",
+    hour        => '*/2',  # every 2 hours
+    minute      => fqdn_rand(60),
+    require     => [
+        Package['puppet'],
+    ],
+    environment => [
+      'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
+    ],
+  }
+}
diff --git a/catatester/puppet/cast_builder/manifests/swarm_init.pp b/catatester/puppet/cast_builder/manifests/swarm_init.pp
new file mode 100644
index 0000000..c2fa507
--- /dev/null
+++ b/catatester/puppet/cast_builder/manifests/swarm_init.pp
@@ -0,0 +1,140 @@
+# Puppet config for Swarming device-bot hosts for CI
+#
+# Instructions to bootstrap/change swarming bot hosts at:
+#   https://g3doc.corp.google.com/chrome/dongle/test/catatester/g3doc/dev/lab_host_changes.md
+
+class cast_builder::swarm_init($env = '', $cipd_env = 'live') {
+
+  # Set up Puppet execution stages
+  stage { 'last': }
+  Stage['main'] -> Stage['last']
+
+  case $env {
+
+    'lab_host': {
+      $user     = 'gtvchrome'
+      $group    = 'gtvchrome'
+      $home_dir = "/home/${user}"
+
+      class { 'cast_builder::swarm_host_base':
+        user     => $user,
+        group    => $group,
+        home_dir => $home_dir,
+      } ->
+      class { 'cast_builder::swarm_host_lab':
+        user     => $user,
+        group    => $group,
+        home_dir => $home_dir,
+      } ->
+      class { 'cast_builder::swarm_bot_home':
+        user     => $user,
+        group    => $group,
+        home_dir => $home_dir,
+      }
+    }
+
+    'gazoo_canary_host' : {
+      $user     = 'gtvchrome'
+      $group    = 'gtvchrome'
+      $home_dir = "/home/${user}"
+
+      class { 'cast_builder::gazoo_host_base':
+        user     => $user,
+        group    => $group,
+        home_dir => $home_dir,
+      }->
+      class { 'cast_builder::gazoo_host_lab':
+        user       => $user,
+        group      => $group,
+        home_dir   => $home_dir,
+        ref_puppet => 'gazoo-staging',
+      }
+    }
+    'gazoo_staging_host' : {
+      $user     = 'gtvchrome'
+      $group    = 'gtvchrome'
+      $home_dir = "/home/${user}"
+
+      class { 'cast_builder::gazoo_host_base':
+        user     => $user,
+        group    => $group,
+        home_dir => $home_dir,
+      }->
+      class { 'cast_builder::gazoo_host_lab':
+        user       => $user,
+        group      => $group,
+        home_dir   => $home_dir,
+        ref_puppet => 'gazoo-staging',
+      }
+    }
+    'gazoo_prod_host' : {
+      $user     = 'gtvchrome'
+      $group    = 'gtvchrome'
+      $home_dir = "/home/${user}"
+
+      class { 'cast_builder::gazoo_host_base':
+        user     => $user,
+        group    => $group,
+        home_dir => $home_dir,
+      }->
+      class { 'cast_builder::gazoo_host_lab':
+        user       => $user,
+        group      => $group,
+        home_dir   => $home_dir,
+        ref_puppet => 'gazoo-prod',
+      }
+    }
+    'gotham_host': {
+      $user     = 'gtvchrome'
+      $group    = 'gtvchrome'
+      $home_dir = "/home/${user}"
+
+      class { 'cast_builder::swarm_host_base':
+        user     => $user,
+        group    => $group,
+        home_dir => $home_dir,
+      } ->
+      class { 'cast_builder::swarm_host_lab':
+        user     => $user,
+        group    => $group,
+        home_dir => $home_dir,
+      } ->
+      class { 'cast_builder::swarm_bot_gotham':
+        user     => $user,
+        group    => $group,
+        home_dir => $home_dir,
+      }
+    }
+
+    'corp_ganeti': {
+      $user     = 'mosaic-role'
+      $group    = 'mosaic-role'
+      $home_dir = "/usr/local/google/home/${user}"
+
+      class { 'cast_builder::swarm_host_base':
+        user     => $user,
+        group    => $group,
+        home_dir => $home_dir,
+      } ->
+      class { 'cast_builder::swarm_host_ganeti':
+        user     => $user,
+        home_dir => $home_dir,
+      } ->
+      class { 'cast_builder::swarm_bot_home':
+        user     => $user,
+        group    => $group,
+        home_dir => $home_dir,
+      }
+
+      # Save last Puppet execution state after all other steps
+      # (only for goobuntu machines).
+      class { 'cast_builder::puppet_state':
+        stage => last,
+      }
+    }
+
+    default: {
+      fail "unknown host environment ${env}"
+    }
+  }
+}
diff --git a/catatester/puppet/cast_builder/manifests/test_md5.pp b/catatester/puppet/cast_builder/manifests/test_md5.pp
new file mode 100644
index 0000000..ffa3caa
--- /dev/null
+++ b/catatester/puppet/cast_builder/manifests/test_md5.pp
@@ -0,0 +1,9 @@
+# Testing file with md5 hash
+# Each unique content file could be tested once per manifest
+define cast_builder::test_md5 ($md5, $path = $title) {
+  exec { "test_${path}":
+    path    => '/bin:/usr/bin',
+    command => "md5sum ${path} | grep ${md5}",
+    user    => root,
+  }
+}
diff --git a/catatester/puppet/cast_builder/templates/ensure_eurtest_binaries.erb b/catatester/puppet/cast_builder/templates/ensure_eurtest_binaries.erb
new file mode 100644
index 0000000..6117f27
--- /dev/null
+++ b/catatester/puppet/cast_builder/templates/ensure_eurtest_binaries.erb
@@ -0,0 +1,4 @@
+home_internal/test_binaries <%= @cipd_ref %>
+home_internal/tool_binaries <%= @cipd_ref %>
+home_internal/virtual_home/cast_shell_emulator_launcher latest
+home_internal/virtual_home/device_linker latest
diff --git a/catatester/puppet/cast_builder/templates/ensure_eurtest_binaries.sh.erb b/catatester/puppet/cast_builder/templates/ensure_eurtest_binaries.sh.erb
new file mode 100644
index 0000000..7559817
--- /dev/null
+++ b/catatester/puppet/cast_builder/templates/ensure_eurtest_binaries.sh.erb
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+if pidof -x -o %PPID "cipd_ensure_eurtest_binaries.sh"; then
+  echo "Eurtest binaries download already running. Skipping this run."
+  exit 0
+fi
+
+<%= @depot_tools_dir %>/cipd ensure -ensure-file <%= @cipd_ensure_eurtest_binaries %> -root <%= @eurtest_binaries %> -service-account-json <%= @cipd_key %>
diff --git a/catatester/puppet/cast_builder/templates/swarm_bot.systemd.erb b/catatester/puppet/cast_builder/templates/swarm_bot.systemd.erb
new file mode 100644
index 0000000..ac3f5f6
--- /dev/null
+++ b/catatester/puppet/cast_builder/templates/swarm_bot.systemd.erb
@@ -0,0 +1,17 @@
+[Unit]
+Description=Startup swarming bot <%= @prefix %><%= @bot_id %>.
+
+[Service]
+User=<%= @user %>
+Group=<%= @group %>
+
+Environment=BOTO_CONFIG=<%= @gsutil_boto %>
+Environment=GOOGLE_APPLICATION_CREDENTIALS=<%= @fragglerock_key %>
+Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:<%= @gsutil_dir %>
+Environment=SWARMING_BOT_ID=<%= @trusted['hostname'] %>-<%= @bot_id %>
+
+# Source: cs.corp.google.com/piper///depot/google3/third_party/luci_py/latest/appengine/swarming/swarming_bot/__main__.py
+ExecStart=/usr/bin/env python <%= @swarm_bots_home %>/<%= @bot_id %>/bot_root/swarming_bot.zip start_bot
+
+[Install]
+WantedBy=multi-user.target
diff --git a/catatester/puppet/cast_builder/templates/swarm_puppet_run.sh.erb b/catatester/puppet/cast_builder/templates/swarm_puppet_run.sh.erb
new file mode 100644
index 0000000..85feb6b
--- /dev/null
+++ b/catatester/puppet/cast_builder/templates/swarm_puppet_run.sh.erb
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+if pidof -x -o %PPID "swarm_puppet_run.sh"; then
+  echo "Puppet run is still in progress. Skipping this run."
+  exit 0
+fi
+
+START=$(date -d now +%s)
+<%= @depot_tools_dir %>/cipd ensure -ensure-file <%= @cipd_ensure_puppet %> -root <%= @puppet_dir %> -service-account-json <%= @swarming_key %> && /usr/bin/apt-get update; /usr/bin/puppet apply --modulepath <%= @puppet_dir %> <%= @puppet_manifest %>
+END=$(date -d now +%s)
+logger PUPPET_RUN_TIME_IN_MINUTES $(( (END - START) / 60 ))
+if (( (END - START) / 60 >= 20 ));
+  then logger PUPPET_RUN_TOO_LONG_WARNING;
+fi
diff --git a/catatester/puppet_cipd_pkg.yaml b/catatester/puppet_cipd_pkg.yaml
new file mode 100644
index 0000000..5491b95
--- /dev/null
+++ b/catatester/puppet_cipd_pkg.yaml
@@ -0,0 +1,17 @@
+package: home_internal/swarm_puppet
+description: puppet manifests and modules for swarm packaged in cipd
+root: puppet
+data:
+  - file: cast_builder/files/swarm/lab_nodes/site.pp
+  - file: cast_builder/lib/facter/devices.rb
+  - file: cast_builder/manifests/swarm_bot_gotham.pp
+  - file: cast_builder/manifests/swarm_bot_home.pp
+  - file: cast_builder/manifests/swarm_host_base.pp
+  - file: cast_builder/manifests/swarm_host_ganeti.pp
+  - file: cast_builder/manifests/swarm_host_lab.pp
+  - file: cast_builder/manifests/swarm_init.pp
+  - file: cast_builder/manifests/test_md5.pp
+  - file: cast_builder/templates/ensure_eurtest_binaries.erb
+  - file: cast_builder/templates/ensure_eurtest_binaries.sh.erb
+  - file: cast_builder/templates/swarm_bot.systemd.erb
+  - file: cast_builder/templates/swarm_puppet_run.sh.erb
diff --git a/catatester/scripts/collectd.conf.template b/catatester/scripts/collectd.conf.template
new file mode 100755
index 0000000..c437594
--- /dev/null
+++ b/catatester/scripts/collectd.conf.template
@@ -0,0 +1,134 @@
+Interval 60
+
+# Explicitly set hostname to "" to indicate the default resource.
+Hostname {{ fqdn }}
+
+# The Stackdriver agent does not use fully qualified domain names.
+FQDNLookup false
+
+LoadPlugin syslog
+<Plugin "syslog">
+  LogLevel "info"
+</Plugin>
+# if you uncomment this, you will get collectd logs separate from syslog
+#LoadPlugin logfile
+#<Plugin "logfile">
+#  LogLevel "info"
+#  File "/var/log/collectd.log"
+#  Timestamp true
+#</Plugin>
+
+# LoadPlugin docker
+LoadPlugin df
+<Plugin "df">
+  FSType "devfs"
+  IgnoreSelected true
+  ReportByDevice true
+  ValuesPercentage true
+</Plugin>
+
+LoadPlugin cpu
+<Plugin "cpu">
+  ValuesPercentage true
+  ReportByCpu false
+</Plugin>
+LoadPlugin swap
+<Plugin "swap">
+  ValuesPercentage true
+</Plugin>
+LoadPlugin interface
+LoadPlugin disk
+LoadPlugin load
+LoadPlugin memory
+<Plugin "memory">
+  ValuesPercentage true
+</Plugin>
+LoadPlugin processes
+LoadPlugin tcpconns
+LoadPlugin write_gcm
+LoadPlugin match_regex
+LoadPlugin match_throttle_metadata_keys
+LoadPlugin stackdriver_agent
+
+<Plugin "processes">
+  ProcessMatch "all" ".*"
+  Detail "ps_cputime"
+  Detail "ps_disk_octets"
+  Detail "ps_rss"
+  Detail "ps_vm"
+</Plugin>
+
+<Plugin "disk">
+  # No config - collectd fails parsing configuration if tag is empty.
+</Plugin>
+
+<Plugin "tcpconns">
+  AllPortsSummary true
+</Plugin>
+
+LoadPlugin exec
+# Monitor the Stackdriver Logging agent. This should fail gracefully if for any
+# reason the metrics endpoint for the Logging agent isn't reachable.
+<Plugin "exec">
+  # The script doesn't need any privileges, so run as 'nobody'.
+  Exec "nobody" "/opt/stackdriver/collectd/bin/stackdriver-read_agent_logging" "http://localhost:24231/metrics"
+</Plugin>
+
+LoadPlugin aggregation
+LoadPlugin "match_regex"
+<Plugin "memory">
+  ValuesPercentage true
+</Plugin>
+
+<Plugin "write_gcm">
+  CloudProvider "gcp"
+  Instance {{ hostname }}
+  Project {{ project_name }}
+  Zone {{ zone_name }}
+  PrettyPrintJSON false
+  # EnableMetadataAgent true
+</Plugin>
+
+PostCacheChain "PostCache"
+<Chain "PostCache">
+  <Rule "processes">
+    <Match "regex">
+      Plugin "^processes$"
+      Type "^(ps_cputime|disk_octets|ps_rss|ps_vm)$"
+    </Match>
+    <Target "jump">
+      Chain "MaybeThrottleProcesses"
+    </Target>
+    Target "stop"
+  </Rule>
+
+  <Rule "otherwise">
+    <Match "throttle_metadata_keys">
+      OKToThrottle false
+    </Match>
+    <Target "write">
+       Plugin "write_gcm"
+    </Target>
+  </Rule>
+</Chain>
+
+<Chain "MaybeThrottleProcesses">
+  <Rule "default">
+    <Match "throttle_metadata_keys">
+      OKToThrottle true
+      TrackedMetadata "processes:pid"
+      TrackedMetadata "processes:command"
+      TrackedMetadata "processes:command_line"
+      TrackedMetadata "processes:owner"
+    </Match>
+    <Target "write">
+       Plugin "write_gcm"
+    </Target>
+  </Rule>
+</Chain>
+
+# if you have other config, especially for plugins, you can drop them
+# into this directory
+Include "/opt/stackdriver/collectd/etc/collectd.d"
+Include "/etc/stackdriver/collectd.d"
+
diff --git a/catatester/scripts/generate_monitoring_configs.py b/catatester/scripts/generate_monitoring_configs.py
new file mode 100755
index 0000000..5e88028
--- /dev/null
+++ b/catatester/scripts/generate_monitoring_configs.py
@@ -0,0 +1,127 @@
+"""Generates collectd and fluentd config from template."""
+import argparse
+import logging
+import os
+import socket
+import subprocess
+
+import jinja2
+
+
+_DEFAULT_PROJECT_ID = 'google.com:fragglerock'
+_DEFAULT_ZONE_NAME = 'us-central1-c'
+_UNINITIALIZED = 'uninitialized'
+
+
+def _GetHostName():
+  """Returns local machine's host name."""
+  return socket.gethostname().split('.')[0]
+
+
+def _GetFqdn():
+  """Returns local machine's FQDN."""
+  return socket.getfqdn()
+
+
+def _GetIpAddress():
+  """Returns IP address of the given interface."""
+  output = subprocess.check_output(['hostname', '-i'])
+  return output.strip()
+
+
+def ParseCmdOptions():
+  """Parses command inputs."""
+  parser = argparse.ArgumentParser(
+      description='Generates collectd and fluentd config.')
+  parser.add_argument('--hostname', help='Host name')
+  parser.add_argument('--fqdn', help='Host FQDN')
+  parser.add_argument('--ip', help='IPv4 address')
+  parser.add_argument(
+      '--project_id', help='Project ID, e.g. google.com:fragglerock')
+  parser.add_argument(
+      '--zone_name', help='Instance zone name, e.g. asia-east1-a')
+  parser.add_argument(
+      '--template_dir', help='Directory containing the templates.')
+  parser.add_argument(
+      '--debug',
+      action='store_true',
+      default=False,
+      help='Print extra debug information.')
+
+  args = parser.parse_args()
+  if args.debug:
+    logging.basicConfig(level=logging.DEBUG)
+  else:
+    logging.basicConfig(level=logging.INFO)
+
+  if not args.hostname or args.hostname == _UNINITIALIZED:
+    args.hostname = _GetHostName()
+  if not args.fqdn or args.fqdn == _UNINITIALIZED:
+    args.fqdn = _GetFqdn()
+  if not args.ip or args.ip == _UNINITIALIZED:
+    args.ip = _GetIpAddress()
+  if not args.project_id or args.project_id == _UNINITIALIZED:
+    args.project_id = _DEFAULT_PROJECT_ID
+  if not args.zone_name or args.zone_name == _UNINITIALIZED:
+    args.zone_name = _DEFAULT_ZONE_NAME
+  if not args.template_dir or not os.path.isdir(args.template_dir):
+    args.template_dir = os.path.dirname(__file__)
+
+  return args
+
+
+def _GenerateCollectdConfig(flags):
+  """Outputs collectd.conf.
+
+  Args:
+    flags: argparse.Namespace instance containing config information.
+  """
+  template_path = os.path.join(flags.template_dir, 'collectd.conf.template')
+  output_path = os.path.join(flags.template_dir, 'collectd.conf')
+  with open(template_path) as fn:
+    tm = jinja2.Template(fn.read())
+    # Values demand double-quotes
+    conf = tm.render(
+        hostname='\"' + flags.hostname + '\"',
+        fqdn='\"' + flags.fqdn + '\"',
+        project_name='\"' + flags.project_id + '\"',
+        zone_name='\"' + flags.zone_name + '\"')
+    with open(output_path, 'w') as output:
+      output.write(conf)
+
+
+def _GenerateFluentdConfig(flags):
+  """Outputs google-fluentd.conf.
+
+  Args:
+    flags: argparse.Namespace instance containing config information.
+  """
+  template_path = os.path.join(flags.template_dir,
+                               'google-fluentd.conf.template')
+  output_path = os.path.join(flags.template_dir,
+                             'google-fluentd.conf')
+  with open(template_path) as fn:
+    tm = jinja2.Template(fn.read())
+    # Values demand double-quotes
+    conf = tm.render(
+        hostname=flags.hostname,
+        fqdn=flags.fqdn,
+        project_id=flags.project_id,
+        zone_name=flags.zone_name)
+    with open(output_path, 'w') as output:
+      output.write(conf)
+
+
+def main():
+  """Generates collectd and fluentd config from template."""
+  flags = ParseCmdOptions()
+  logging.debug('hostname: %s', flags.hostname)
+  logging.debug('fqdn: %s', flags.fqdn)
+  logging.debug('project: %s', flags.project_id)
+  logging.debug('zone: %s', flags.zone_name)
+  _GenerateCollectdConfig(flags)
+  _GenerateFluentdConfig(flags)
+
+
+if __name__ == '__main__':
+  main()
diff --git a/catatester/scripts/google-fluentd.conf.template b/catatester/scripts/google-fluentd.conf.template
new file mode 100755
index 0000000..c5eaaf2
--- /dev/null
+++ b/catatester/scripts/google-fluentd.conf.template
@@ -0,0 +1,64 @@
+# Master configuration file for google-fluentd
+
+# Include any configuration files in the config.d directory.
+#
+# An example "catch-all" configuration can be found at
+# https://github.com/GoogleCloudPlatform/fluentd-catch-all-config
+@include config.d/*.conf
+
+# Prometheus monitoring.
+<source>
+  @type prometheus
+  port 24231
+</source>
+<source>
+  @type prometheus_monitor
+</source>
+
+# Do not collect fluentd's own logs to avoid infinite loops.
+<match fluent.**>
+  @type null
+</match>
+
+# Add a unique insertId to each log entry that doesn't already have it.
+# This helps guarantee the order and prevent log duplication.
+<filter **>
+  @type add_insert_ids
+</filter>
+
+# Configure all sources to output to Google Cloud Logging
+<match **>
+  @type google_cloud
+  # Set the chunk limit conservatively to avoid exceeding the recommended
+  # chunk size of 5MB per write request.
+  buffer_chunk_limit 1M
+  # Flush logs every 5 seconds, even if the buffer is not full.
+  flush_interval 5s
+  # Enforce some limit on the number of retries.
+  disable_retry_limit false
+  # After 3 retries, a given chunk will be discarded.
+  retry_limit 3
+  # Wait 10 seconds before the first retry. The wait interval will be doubled on
+  # each following retry (20s, 40s...) until it hits the retry limit.
+  retry_wait 10
+  # Never wait longer than 5 minutes between retries. If the wait interval
+  # reaches this limit, the exponentiation stops.
+  # Given the default config, this limit should never be reached, but if
+  # retry_limit and retry_wait are customized, this limit might take effect.
+  max_retry_wait 300
+  # Use multiple threads for processing.
+  num_threads 8
+  # Use the gRPC transport.
+  use_grpc true
+  # If a request is a mix of valid log entries and invalid ones, ingest the
+  # valid ones and drop the invalid ones instead of dropping everything.
+  partial_success true
+  # Enable monitoring via Prometheus integration.
+  enable_monitoring true
+  monitoring_type prometheus
+  project_id {{ project_id }}
+  vm_id {{ fqdn }}
+  vm_name {{ hostname }}
+  zone {{ zone_name }}
+</match>
+
diff --git a/catatester/scripts/install_stackdriver_agent.sh b/catatester/scripts/install_stackdriver_agent.sh
new file mode 100755
index 0000000..b7f215b
--- /dev/null
+++ b/catatester/scripts/install_stackdriver_agent.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+#
+# Script to install Cloud Monitoring and Logging Agent on lab host.
+#
+# Assumptions:
+#   The Cloud Project service account key file ($HOME/luci/swarming-key.json)
+#   is available. This is used to upload metrics and logs to the Cloud Project.
+#
+# Prerequisites:
+#   All companion python modules are copied to same directory.
+
+set -e
+
+function print_help {
+  echo -e "
+    Usage: $0 -n <hostname> -f <fqdn> -p <project_id> -z <zone_name>
+    Available options:
+    \t-n\thostname
+    \t-f\tfully qualified domain name
+    \t-p\tproject id
+    \t-z\tzone name
+    \t-?,-h\tHelp
+  "
+}
+
+hostname="uninitialized"
+fqdn="uninitialized"
+ip="uninitialized"
+project_id="uninitialized"
+zone_name="uninitialized"
+
+while getopts "?h:n:f:i:p:z:" opt
+do
+  case $opt in
+    n) hostname=$OPTARG;;
+    f) fqdn=$OPTARG;;
+    i) ip=$OPTARG;;
+    p) project_id=$OPTARG;;
+    z) zone_name=$OPTARG;;
+    ?|h) print_help; exit 1;;
+  esac
+done
+shift $((OPTIND-1))
+
+root_dir="$( cd "$(dirname "$0")" ; pwd -P )"
+config_transformer="$root_dir/generate_monitoring_configs.py"
+
+# Install jinja2
+pip install jinja2
+
+# Set up GCP service account credential
+echo "Setting up GCP credentials - started"
+APPLICATION_DEFAULT_CREDS="/etc/google/auth/application_default_credentials.json"
+sudo mkdir -p /etc/google/auth
+sudo cp "$HOME/luci/swarming-key.json" "$APPLICATION_DEFAULT_CREDS"
+sudo chown root:root "$APPLICATION_DEFAULT_CREDS"
+sudo chmod 0400 "$APPLICATION_DEFAULT_CREDS"
+echo "Setting up GCP credentials - success"
+
+# Install Monitoring Agent
+echo "Installing monitoring agent - started"
+cd $HOME
+curl -sSO https://dl.google.com/cloudagents/install-monitoring-agent.sh
+sudo env REPO_SUFFIX=unstable bash install-monitoring-agent.sh
+echo "installing monitoring agent - success"
+
+# Install Logging Agent
+echo "Installing logging agent - started"
+cd $HOME
+curl -sSO https://dl.google.com/cloudagents/install-logging-agent.sh
+sudo env REPO_SUFFIX=unstable bash install-logging-agent.sh
+echo "Installing logging agent - success"
+
+/usr/bin/python $config_transformer --debug --hostname $hostname --fqdn $fqdn --ip $ip --project_id $project_id --zone_name $zone_name
+sudo cp $root_dir/collectd.conf /etc/stackdriver/collectd.conf
+sudo cp $root_dir/google-fluentd.conf /etc/google-fluentd/google-fluentd.conf
+sudo service stackdriver-agent restart
+sudo service google-fluentd restart
diff --git a/catatester/scripts/setup_swarm_host.sh b/catatester/scripts/setup_swarm_host.sh
new file mode 100755
index 0000000..5cdebe8
--- /dev/null
+++ b/catatester/scripts/setup_swarm_host.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+#
+# Script to setup a host for running in Home Swarming CI.
+#
+# Prereqs:
+#   this script and its dependencies are scp'd to the swarm lab host via:
+#     $ scp /google/data/ro/teams/cast-test/swarming/* gtvchrome@<lab-host-ip>:/home/gtvchrome/
+#        (gtvchrome password in valentine)
+#   all of these shell scripts have chmod +x on the lab host:  chmod +x *.sh
+#   you're ssh'd into the lab host and ready to run this script on it
+#
+# Usage:
+#  ./setup_swarm_host.sh
+
+# stop any bots that might be running already
+for abspath in /etc/init/swarming*.conf; do
+  filename=$(basename "$abspath")
+  botname="${filename%.*}"
+  sudo stop "$botname"
+done
+
+# our puppet config requires at least 1 bot dir set up already
+echo -e "Visit the following URL to get the latest token code and copy-paste it below.\n"
+echo -e "https://cast-swarming.appspot.com\n"
+read -p "Token Code: " token
+SWARM_HOME=~/swarm_home
+if [ ! -d "$SWARM_HOME" ]; then
+  mkdir $SWARM_HOME
+fi
+cd $SWARM_HOME
+rm -rf bots ; mkdir -p bots/1 ; cd bots/1
+curl -sSLOJ https://cast-swarming.appspot.com/bot_code?tok=$token
+
+# Fetch a temporary copy of our puppet configs from cipd and run it.
+# The puppet config will set up a permanent puppet config dir and keep it up-to-date with cron.
+source $HOME/install_cipd.sh $HOME/depot_tools/
+
+# Init masterless puppet config only on gtvchrome lab hosts.
+# Ganeti hosts are configured via ENC puppet (mosaic-role user, not gtvchrome)
+if [[ "$PWD" =~ gtvchrome ]]; then
+  rm -rf /tmp/puppet; mkdir /tmp/puppet; cd /tmp/puppet
+  cipd install home_internal/swarm_puppet staging -service-account-json ~/luci/swarming-key.json
+
+  sudo apt-get install --yes puppet
+  sudo /usr/bin/puppet apply --modulepath ./ ./cast_builder/files/swarm/lab_nodes/site.pp
+fi
diff --git a/configs/pre-commit/confidential-terms.txt b/configs/pre-commit/confidential-terms.txt
new file mode 100644
index 0000000..4a2e35c
--- /dev/null
+++ b/configs/pre-commit/confidential-terms.txt
@@ -0,0 +1,9 @@
+prince
+gfiber
+marvell
+mstar
+mediatek
+jbl
+sigma
+vizio
+amlogic
diff --git a/configs/pre-commit/cppcheck/cppcheck.cfg b/configs/pre-commit/cppcheck/cppcheck.cfg
new file mode 100644
index 0000000..9027845
--- /dev/null
+++ b/configs/pre-commit/cppcheck/cppcheck.cfg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- when cppcheck 1.82 is introduced, it is identified putting second macro with in the same named namespace causes problem. b/127266463. This customize cfg file is to work around this problem. Further details please see http://cppcheck.sourceforge.net/manual.pdf Chapter 10. -->
+
+<def version="2">
+  <define name="TEST_F" value=""/>
+  <define name="MATCHER_P2" value=""/>
+  <define name="MATCHER_P" value=""/>
+</def>
diff --git a/configs/pre-commit/requirements.in b/configs/pre-commit/requirements.in
new file mode 100644
index 0000000..917f9d4
--- /dev/null
+++ b/configs/pre-commit/requirements.in
@@ -0,0 +1 @@
+pre-commit==2.12.0
diff --git a/configs/pre-commit/requirements.txt b/configs/pre-commit/requirements.txt
new file mode 100644
index 0000000..450fa14
--- /dev/null
+++ b/configs/pre-commit/requirements.txt
@@ -0,0 +1,28 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+#    pip-compile requirements.in
+#
+appdirs==1.4.4
+    # via virtualenv
+cfgv==3.2.0
+    # via pre-commit
+distlib==0.3.1
+    # via virtualenv
+filelock==3.0.12
+    # via virtualenv
+identify==2.2.2
+    # via pre-commit
+nodeenv==1.5.0
+    # via pre-commit
+pre-commit==2.12.0
+    # via -r requirements.in
+pyyaml==5.4.1
+    # via pre-commit
+six==1.15.0
+    # via virtualenv
+toml==0.10.2
+    # via pre-commit
+virtualenv==20.4.3
+    # via pre-commit
diff --git a/cq/__init__.py b/cq/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cq/__init__.py
diff --git a/cq/appengine/Makefile b/cq/appengine/Makefile
new file mode 100644
index 0000000..63ccf2f
--- /dev/null
+++ b/cq/appengine/Makefile
@@ -0,0 +1,21 @@
+# Makefile to update protobuf objects
+#
+# Every time the protos are updated, just run:
+#   $ make
+
+CWD=$(shell pwd)
+
+PROTOFOLDER=$(CWD)/proto
+OUTPUT_FOLDER=$(CWD)/genfiles
+
+.PHONY: proto
+
+all: proto
+
+gen_proto:
+	protoc -I$(PROTOFOLDER) --python_out=$(OUTPUT_FOLDER) $(PROTOFOLDER)/*.proto
+
+gen_init:
+	touch $(OUTPUT_FOLDER)/__init__.py
+
+proto: gen_proto gen_init
diff --git a/cq/appengine/README.md b/cq/appengine/README.md
new file mode 100644
index 0000000..103d43a
--- /dev/null
+++ b/cq/appengine/README.md
@@ -0,0 +1,19 @@
+# Updating the protos
+
+Just run `make` and the generated protos will be updated.
+
+If the protoc compiler is confused by the map type, you are probably
+running version of protoc < v3.0. You can check with `protoc --version`. See
+below for updating your protoc version.
+
+# Installing protoc v3 compiler
+
+Grab protoc-3.2.0-linux-x86_64.zip release from
+https://github.com/google/protobuf/releases/tag/v3.2.0
+
+Newer releases of protoc are not compatible with the current version of protobuf python package
+installed in requirements.txt at the time of this writing.
+
+Extract `bin/protoc` from the release and copy it into /usr/bin/protoc. Give it execute permissions.
+
+Check the new version with `protoc --version`.
diff --git a/cq/appengine/__init__.py b/cq/appengine/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cq/appengine/__init__.py
diff --git a/cq/appengine/config_test.py b/cq/appengine/config_test.py
new file mode 100644
index 0000000..726de1a
--- /dev/null
+++ b/cq/appengine/config_test.py
@@ -0,0 +1,162 @@
+"""Tests for config."""
+
+import os
+import sys
+import unittest
+
+# Allow imports to be rooted in continuous-tests/ folder
+sys.path.insert(
+    0,
+    os.path.realpath(
+        os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)))
+
+# pylint: disable=wrong-import-position
+from cq.appengine.genfiles import build_config_pb2
+
+import google.protobuf.text_format
+
+_BUILD_RULES_RELPATH = 'configs/{env}/build_rules.protoascii'
+_BUILD_DETAILS_RELPATH = 'configs/{env}/build_details.protoascii'
+
+
+def GetBuildRules(build_config_env):
+  """Returns the BuildRules proto."""
+  build_rules = build_config_pb2.BuildRules()
+  build_rules_path = os.path.join(
+      os.path.dirname(__file__),
+      _BUILD_RULES_RELPATH.format(env=build_config_env))
+  with open(build_rules_path) as f:
+    google.protobuf.text_format.Merge(f.read(), build_rules)
+  return build_rules
+
+
+def GetBuildDetails(build_config_env):
+  """Returns the BuildDetails proto."""
+  build_details = build_config_pb2.BuildDetails()
+  build_details_path = os.path.join(
+      os.path.dirname(__file__),
+      _BUILD_DETAILS_RELPATH.format(env=build_config_env))
+  with open(build_details_path) as f:
+    google.protobuf.text_format.Merge(f.read(), build_details)
+  return build_details
+
+# Use a wrapping class so the test runner does not mistakenly try to run
+# tests on ConfigTest, which is not a fully-implemented test class
+class BaseTestCases:
+  class ConfigTest(unittest.TestCase):
+    """A class to test a build config.
+
+    Note: The |self.env| property is initialized externally to this class. This
+    class will be setup twice, once with the staging config and once with the
+    prod config.
+    """
+    env = None
+
+    @property
+    def build_rules_proto(self):
+      return GetBuildRules(self.env)
+
+    @property
+    def build_details_proto(self):
+      return GetBuildDetails(self.env)
+
+    def testParseBuildRules(self):
+      """Tests the build_rules config is a valid proto."""
+      self.assertTrue(self.build_rules_proto)
+
+    def testParseBuildDetails(self):
+      """Tests the build_details config is a valid proto."""
+      self.assertTrue(self.build_details_proto)
+
+    def testOnlySupportedGobHosts(self):
+      """Tests that only supported gob_hosts are used."""
+      supported_hosts = {
+          'caraway-internal',
+          'chrome-internal',
+          'eureka-internal',
+          'eureka-partner',
+          'eureka-staging-internal',
+          'libassistant-internal',
+          'nest-camera-internal',
+          'nest-internal',
+          'team',
+      }
+      for rule in self.build_rules_proto.build_rules:
+        self.assertIn(rule.gob_host, supported_hosts)
+
+    def testEveryBuildRuleHasAMatchingBuildDetail(self):
+      """Test that every build in a build_rule has a matching build_detail."""
+
+      referenced_builds = set()
+      for rule in self.build_rules_proto.build_rules:
+        referenced_builds.update(rule.builds)
+        referenced_builds.update(rule.presubmit_builds)
+        for build_set in rule.build_sets:
+          referenced_builds.update(
+              self.build_rules_proto.build_sets[build_set].builds)
+
+      build_detail_builds = set()
+      for build_name in self.build_details_proto.build_details:
+        build_detail_builds.add(build_name)
+
+      # Note that it is acceptable for there to be extra build_details, since
+      # that is how a build is enabled for manually scheduling.
+      missing_build_details = referenced_builds - build_detail_builds
+      self.assertFalse(missing_build_details)
+
+    def testScheduleValid(self):
+      for build, detail in self.build_details_proto.build_details.items():
+        if detail.schedule.min_branch and detail.schedule.max_branch:
+          self.assertLess(
+              detail.schedule.min_branch, detail.schedule.max_branch,
+              'Branch range ({} < {}) for {} is invalid'.format(
+                  detail.schedule.min_branch, detail.schedule.max_branch, build
+              )
+          )
+        if detail.schedule.master_only and detail.schedule.release_only:
+          raise AssertionError(
+              'Can only set master only or release only for {}'.format(build)
+          )
+
+    def testHasRequiredFields(self):
+      for name, detail in self.build_details_proto.build_details.items():
+        self.assertTrue(
+            detail.HasField('config'),
+            f'{name} must have oneof field config set.')
+        which_config = detail.WhichOneof('config')
+        if which_config == 'device_tests':
+          self.assertGreater(
+              len(detail.device_tests.test_groups),
+              0,
+              f'{name} must have at least one test_group')
+          self.assertTrue(
+              detail.device_tests.HasField('depends_on'),
+              f'{name} must have a depends_on set')
+          self.assertGreater(
+              len(detail.device_tests.depends_on),
+              0,
+              f'{name} must have a non-empty depends_on')
+        elif which_config == 'cast_build':
+          self.assertTrue(
+              detail.cast_build.HasField('recipe'),
+              f'{name} must have a recipe set')
+          self.assertGreater(
+              len(detail.cast_build.recipe),
+              0,
+              f'{name} must have a non-empty recipe')
+        elif which_config == 'jenkins_job':
+          self.assertTrue(
+              detail.jenkins_job.HasField('job_name'),
+              f'{name} must have a job_name set')
+          self.assertGreater(
+              len(detail.jenkins_job.job_name),
+              0,
+              f'{name} must have a non-empty job_name')
+
+
+class ProdConfigTest(BaseTestCases.ConfigTest):
+  env = "prod"
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/appengine/configs/prod/build_details.protoascii b/cq/appengine/configs/prod/build_details.protoascii
new file mode 100644
index 0000000..107edbf
--- /dev/null
+++ b/cq/appengine/configs/prod/build_details.protoascii
@@ -0,0 +1,29 @@
+#
+#                                         ██████████                                      
+#                                    ████          ████                                  
+#                                  ██      ▒▒▒▒▒▒      ██                                
+#                                ██      ▒▒▒▒▒▒▒▒▒▒      ██                              
+#                                ██      ▒▒▒▒▒▒▒▒▒▒        ██                            
+#                              ██▒▒      ▒▒▒▒▒▒▒▒▒▒      ▒▒██                            
+#                              ██▒▒▒▒      ▒▒▒▒▒▒        ▒▒▒▒██                          
+#                              ██▒▒▒▒                    ▒▒▒▒██                          
+#                              ██▒▒      ████████        ▒▒▒▒██                          
+#                              ██    ██████░░░░██████      ▒▒██                          
+#                              ██  ██░░░░██░░░░██░░░░██      ██                          
+#                                ████░░░░██░░░░██░░░░░░██  ██                            
+#                                  ██░░░░░░░░░░░░░░░░░░██  ██                            
+#                                  ██░░░░████████░░░░░░████                              
+#                                    ██░░░░░░░░░░░░░░████                                
+#                                    ████████████████████                                
+#                                  ████▓▓██░░░░██▓▓██░░░░██                              
+#                                ██░░██▓▓██░░░░██▓▓▓▓██░░░░██                            
+#                              ██░░██▓▓██░░░░░░██▓▓▓▓██░░░░██                            
+#                              ██░░██▓▓██████████▓▓▓▓▓▓██░░░░██                          
+#                              ██░░██▓▓██        ██▓▓▓▓██░░░░██                          
+#                                ██████    ██      ██████████                            
+#                                    ██      ██        ██                                
+#                                  ██▓▓▓▓▓▓████▓▓▓▓██████                                
+#                                ██▓▓▓▓▓▓▓▓██▓▓▓▓▓▓▓▓▓▓██                                
+#                                ████████████████████████        
+#
+# Sorry, config is in another castle. go/cast-config-migration
diff --git a/cq/appengine/configs/prod/build_rules.protoascii b/cq/appengine/configs/prod/build_rules.protoascii
new file mode 100644
index 0000000..107edbf
--- /dev/null
+++ b/cq/appengine/configs/prod/build_rules.protoascii
@@ -0,0 +1,29 @@
+#
+#                                         ██████████                                      
+#                                    ████          ████                                  
+#                                  ██      ▒▒▒▒▒▒      ██                                
+#                                ██      ▒▒▒▒▒▒▒▒▒▒      ██                              
+#                                ██      ▒▒▒▒▒▒▒▒▒▒        ██                            
+#                              ██▒▒      ▒▒▒▒▒▒▒▒▒▒      ▒▒██                            
+#                              ██▒▒▒▒      ▒▒▒▒▒▒        ▒▒▒▒██                          
+#                              ██▒▒▒▒                    ▒▒▒▒██                          
+#                              ██▒▒      ████████        ▒▒▒▒██                          
+#                              ██    ██████░░░░██████      ▒▒██                          
+#                              ██  ██░░░░██░░░░██░░░░██      ██                          
+#                                ████░░░░██░░░░██░░░░░░██  ██                            
+#                                  ██░░░░░░░░░░░░░░░░░░██  ██                            
+#                                  ██░░░░████████░░░░░░████                              
+#                                    ██░░░░░░░░░░░░░░████                                
+#                                    ████████████████████                                
+#                                  ████▓▓██░░░░██▓▓██░░░░██                              
+#                                ██░░██▓▓██░░░░██▓▓▓▓██░░░░██                            
+#                              ██░░██▓▓██░░░░░░██▓▓▓▓██░░░░██                            
+#                              ██░░██▓▓██████████▓▓▓▓▓▓██░░░░██                          
+#                              ██░░██▓▓██        ██▓▓▓▓██░░░░██                          
+#                                ██████    ██      ██████████                            
+#                                    ██      ██        ██                                
+#                                  ██▓▓▓▓▓▓████▓▓▓▓██████                                
+#                                ██▓▓▓▓▓▓▓▓██▓▓▓▓▓▓▓▓▓▓██                                
+#                                ████████████████████████        
+#
+# Sorry, config is in another castle. go/cast-config-migration
diff --git a/cq/appengine/genfiles/__init__.py b/cq/appengine/genfiles/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cq/appengine/genfiles/__init__.py
diff --git a/cq/appengine/genfiles/build_config_pb2.py b/cq/appengine/genfiles/build_config_pb2.py
new file mode 100644
index 0000000..d0776cc
--- /dev/null
+++ b/cq/appengine/genfiles/build_config_pb2.py
@@ -0,0 +1,736 @@
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: build_config.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='build_config.proto',
+  package='build_config',
+  syntax='proto2',
+  serialized_pb=_b('\n\x12\x62uild_config.proto\x12\x0c\x62uild_config\"\xbd\x02\n\tBuildRule\x12\x10\n\x08gob_host\x18\x01 \x01(\t\x12\x0e\n\x06\x62ranch\x18\x03 \x01(\t\x12\x0f\n\x07project\x18\x04 \x01(\t\x12\x18\n\x10presubmit_builds\x18\x05 \x03(\t\x12\x0e\n\x06\x62uilds\x18\x06 \x03(\t\x12\x12\n\nbuild_sets\x18\x07 \x03(\t\x12J\n\x12property_overrides\x18\x08 \x03(\x0b\x32..build_config.BuildRule.PropertyOverridesEntry\x12\x16\n\x0e\x66ile_whitelist\x18\t \x03(\t\x1aU\n\x16PropertyOverridesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0b\x32\x1b.build_config.PropertyValue:\x02\x38\x01J\x04\x08\x02\x10\x03\":\n\rPropertyValue\x12\x10\n\x06string\x18\x01 \x01(\tH\x00\x12\x0e\n\x04\x62ool\x18\x02 \x01(\x08H\x00\x42\x07\n\x05value\"\x1a\n\x08\x42uildSet\x12\x0e\n\x06\x62uilds\x18\x01 \x03(\t\"\xc1\x01\n\nBuildRules\x12,\n\x0b\x62uild_rules\x18\x01 \x03(\x0b\x32\x17.build_config.BuildRule\x12;\n\nbuild_sets\x18\x02 \x03(\x0b\x32\'.build_config.BuildRules.BuildSetsEntry\x1aH\n\x0e\x42uildSetsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.build_config.BuildSet:\x02\x38\x01\"\x1e\n\nJenkinsJob\x12\x10\n\x08job_name\x18\x01 \x01(\t\"1\n\tCastBuild\x12\x0e\n\x06recipe\x18\x01 \x01(\t\x12\x14\n\x0c\x62uilder_name\x18\x02 \x01(\t\"\xd3\x04\n\x0b\x42uildDetail\x12\x16\n\x08required\x18\x01 \x01(\x08:\x04true\x12\x19\n\x11\x64isabled_branches\x18\x03 \x03(\t\x12L\n\x12property_overrides\x18\x05 \x03(\x0b\x32\x30.build_config.BuildDetail.PropertyOverridesEntry\x12?\n\x08schedule\x18\x07 \x01(\x0b\x32-.build_config.BuildDetail.ScheduleRestriction\x12\r\n\x05owner\x18\x0c \x01(\t\x12-\n\ncast_build\x18\x10 \x01(\x0b\x32\x17.build_config.CastBuildH\x00\x12/\n\x0bjenkins_job\x18\x11 \x01(\x0b\x32\x18.build_config.JenkinsJobH\x00\x1aU\n\x16PropertyOverridesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0b\x32\x1b.build_config.PropertyValue:\x02\x38\x01\x1av\n\x13ScheduleRestriction\x12\x12\n\nmin_branch\x18\x01 \x01(\t\x12\x12\n\nmax_branch\x18\x02 \x01(\t\x12\x1b\n\x0crelease_only\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x0bmaster_only\x18\x04 \x01(\x08:\x05\x66\x61lseB\x08\n\x06\x63onfigJ\x04\x08\x02\x10\x03J\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\tJ\x04\x08\t\x10\nJ\x04\x08\n\x10\x0bJ\x04\x08\x0b\x10\x0cJ\x04\x08\r\x10\x0eJ\x04\x08\x0e\x10\x0fJ\x04\x08\x0f\x10\x10\"\xa3\x01\n\x0c\x42uildDetails\x12\x43\n\rbuild_details\x18\x01 \x03(\x0b\x32,.build_config.BuildDetails.BuildDetailsEntry\x1aN\n\x11\x42uildDetailsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12(\n\x05value\x18\x02 \x01(\x0b\x32\x19.build_config.BuildDetail:\x02\x38\x01')
+)
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+
+
+
+_BUILDRULE_PROPERTYOVERRIDESENTRY = _descriptor.Descriptor(
+  name='PropertyOverridesEntry',
+  full_name='build_config.BuildRule.PropertyOverridesEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='build_config.BuildRule.PropertyOverridesEntry.key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='build_config.BuildRule.PropertyOverridesEntry.value', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=263,
+  serialized_end=348,
+)
+
+_BUILDRULE = _descriptor.Descriptor(
+  name='BuildRule',
+  full_name='build_config.BuildRule',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='gob_host', full_name='build_config.BuildRule.gob_host', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='branch', full_name='build_config.BuildRule.branch', index=1,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='project', full_name='build_config.BuildRule.project', index=2,
+      number=4, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='presubmit_builds', full_name='build_config.BuildRule.presubmit_builds', index=3,
+      number=5, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='builds', full_name='build_config.BuildRule.builds', index=4,
+      number=6, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='build_sets', full_name='build_config.BuildRule.build_sets', index=5,
+      number=7, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='property_overrides', full_name='build_config.BuildRule.property_overrides', index=6,
+      number=8, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='file_whitelist', full_name='build_config.BuildRule.file_whitelist', index=7,
+      number=9, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[_BUILDRULE_PROPERTYOVERRIDESENTRY, ],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=37,
+  serialized_end=354,
+)
+
+
+_PROPERTYVALUE = _descriptor.Descriptor(
+  name='PropertyValue',
+  full_name='build_config.PropertyValue',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='string', full_name='build_config.PropertyValue.string', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='bool', full_name='build_config.PropertyValue.bool', index=1,
+      number=2, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+    _descriptor.OneofDescriptor(
+      name='value', full_name='build_config.PropertyValue.value',
+      index=0, containing_type=None, fields=[]),
+  ],
+  serialized_start=356,
+  serialized_end=414,
+)
+
+
+_BUILDSET = _descriptor.Descriptor(
+  name='BuildSet',
+  full_name='build_config.BuildSet',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='builds', full_name='build_config.BuildSet.builds', index=0,
+      number=1, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=416,
+  serialized_end=442,
+)
+
+
+_BUILDRULES_BUILDSETSENTRY = _descriptor.Descriptor(
+  name='BuildSetsEntry',
+  full_name='build_config.BuildRules.BuildSetsEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='build_config.BuildRules.BuildSetsEntry.key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='build_config.BuildRules.BuildSetsEntry.value', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=566,
+  serialized_end=638,
+)
+
+_BUILDRULES = _descriptor.Descriptor(
+  name='BuildRules',
+  full_name='build_config.BuildRules',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='build_rules', full_name='build_config.BuildRules.build_rules', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='build_sets', full_name='build_config.BuildRules.build_sets', index=1,
+      number=2, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[_BUILDRULES_BUILDSETSENTRY, ],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=445,
+  serialized_end=638,
+)
+
+
+_JENKINSJOB = _descriptor.Descriptor(
+  name='JenkinsJob',
+  full_name='build_config.JenkinsJob',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='job_name', full_name='build_config.JenkinsJob.job_name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=640,
+  serialized_end=670,
+)
+
+
+_CASTBUILD = _descriptor.Descriptor(
+  name='CastBuild',
+  full_name='build_config.CastBuild',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='recipe', full_name='build_config.CastBuild.recipe', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='builder_name', full_name='build_config.CastBuild.builder_name', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=672,
+  serialized_end=721,
+)
+
+
+_BUILDDETAIL_PROPERTYOVERRIDESENTRY = _descriptor.Descriptor(
+  name='PropertyOverridesEntry',
+  full_name='build_config.BuildDetail.PropertyOverridesEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='build_config.BuildDetail.PropertyOverridesEntry.key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='build_config.BuildDetail.PropertyOverridesEntry.value', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=263,
+  serialized_end=348,
+)
+
+_BUILDDETAIL_SCHEDULERESTRICTION = _descriptor.Descriptor(
+  name='ScheduleRestriction',
+  full_name='build_config.BuildDetail.ScheduleRestriction',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='min_branch', full_name='build_config.BuildDetail.ScheduleRestriction.min_branch', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='max_branch', full_name='build_config.BuildDetail.ScheduleRestriction.max_branch', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='release_only', full_name='build_config.BuildDetail.ScheduleRestriction.release_only', index=2,
+      number=3, type=8, cpp_type=7, label=1,
+      has_default_value=True, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='master_only', full_name='build_config.BuildDetail.ScheduleRestriction.master_only', index=3,
+      number=4, type=8, cpp_type=7, label=1,
+      has_default_value=True, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1131,
+  serialized_end=1249,
+)
+
+_BUILDDETAIL = _descriptor.Descriptor(
+  name='BuildDetail',
+  full_name='build_config.BuildDetail',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='required', full_name='build_config.BuildDetail.required', index=0,
+      number=1, type=8, cpp_type=7, label=1,
+      has_default_value=True, default_value=True,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='disabled_branches', full_name='build_config.BuildDetail.disabled_branches', index=1,
+      number=3, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='property_overrides', full_name='build_config.BuildDetail.property_overrides', index=2,
+      number=5, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='schedule', full_name='build_config.BuildDetail.schedule', index=3,
+      number=7, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='owner', full_name='build_config.BuildDetail.owner', index=4,
+      number=12, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='cast_build', full_name='build_config.BuildDetail.cast_build', index=5,
+      number=16, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='jenkins_job', full_name='build_config.BuildDetail.jenkins_job', index=6,
+      number=17, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[_BUILDDETAIL_PROPERTYOVERRIDESENTRY, _BUILDDETAIL_SCHEDULERESTRICTION, ],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+    _descriptor.OneofDescriptor(
+      name='config', full_name='build_config.BuildDetail.config',
+      index=0, containing_type=None, fields=[]),
+  ],
+  serialized_start=724,
+  serialized_end=1319,
+)
+
+
+_BUILDDETAILS_BUILDDETAILSENTRY = _descriptor.Descriptor(
+  name='BuildDetailsEntry',
+  full_name='build_config.BuildDetails.BuildDetailsEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='build_config.BuildDetails.BuildDetailsEntry.key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='build_config.BuildDetails.BuildDetailsEntry.value', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1407,
+  serialized_end=1485,
+)
+
+_BUILDDETAILS = _descriptor.Descriptor(
+  name='BuildDetails',
+  full_name='build_config.BuildDetails',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='build_details', full_name='build_config.BuildDetails.build_details', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[_BUILDDETAILS_BUILDDETAILSENTRY, ],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1322,
+  serialized_end=1485,
+)
+
+_BUILDRULE_PROPERTYOVERRIDESENTRY.fields_by_name['value'].message_type = _PROPERTYVALUE
+_BUILDRULE_PROPERTYOVERRIDESENTRY.containing_type = _BUILDRULE
+_BUILDRULE.fields_by_name['property_overrides'].message_type = _BUILDRULE_PROPERTYOVERRIDESENTRY
+_PROPERTYVALUE.oneofs_by_name['value'].fields.append(
+  _PROPERTYVALUE.fields_by_name['string'])
+_PROPERTYVALUE.fields_by_name['string'].containing_oneof = _PROPERTYVALUE.oneofs_by_name['value']
+_PROPERTYVALUE.oneofs_by_name['value'].fields.append(
+  _PROPERTYVALUE.fields_by_name['bool'])
+_PROPERTYVALUE.fields_by_name['bool'].containing_oneof = _PROPERTYVALUE.oneofs_by_name['value']
+_BUILDRULES_BUILDSETSENTRY.fields_by_name['value'].message_type = _BUILDSET
+_BUILDRULES_BUILDSETSENTRY.containing_type = _BUILDRULES
+_BUILDRULES.fields_by_name['build_rules'].message_type = _BUILDRULE
+_BUILDRULES.fields_by_name['build_sets'].message_type = _BUILDRULES_BUILDSETSENTRY
+_BUILDDETAIL_PROPERTYOVERRIDESENTRY.fields_by_name['value'].message_type = _PROPERTYVALUE
+_BUILDDETAIL_PROPERTYOVERRIDESENTRY.containing_type = _BUILDDETAIL
+_BUILDDETAIL_SCHEDULERESTRICTION.containing_type = _BUILDDETAIL
+_BUILDDETAIL.fields_by_name['property_overrides'].message_type = _BUILDDETAIL_PROPERTYOVERRIDESENTRY
+_BUILDDETAIL.fields_by_name['schedule'].message_type = _BUILDDETAIL_SCHEDULERESTRICTION
+_BUILDDETAIL.fields_by_name['cast_build'].message_type = _CASTBUILD
+_BUILDDETAIL.fields_by_name['jenkins_job'].message_type = _JENKINSJOB
+_BUILDDETAIL.oneofs_by_name['config'].fields.append(
+  _BUILDDETAIL.fields_by_name['cast_build'])
+_BUILDDETAIL.fields_by_name['cast_build'].containing_oneof = _BUILDDETAIL.oneofs_by_name['config']
+_BUILDDETAIL.oneofs_by_name['config'].fields.append(
+  _BUILDDETAIL.fields_by_name['jenkins_job'])
+_BUILDDETAIL.fields_by_name['jenkins_job'].containing_oneof = _BUILDDETAIL.oneofs_by_name['config']
+_BUILDDETAILS_BUILDDETAILSENTRY.fields_by_name['value'].message_type = _BUILDDETAIL
+_BUILDDETAILS_BUILDDETAILSENTRY.containing_type = _BUILDDETAILS
+_BUILDDETAILS.fields_by_name['build_details'].message_type = _BUILDDETAILS_BUILDDETAILSENTRY
+DESCRIPTOR.message_types_by_name['BuildRule'] = _BUILDRULE
+DESCRIPTOR.message_types_by_name['PropertyValue'] = _PROPERTYVALUE
+DESCRIPTOR.message_types_by_name['BuildSet'] = _BUILDSET
+DESCRIPTOR.message_types_by_name['BuildRules'] = _BUILDRULES
+DESCRIPTOR.message_types_by_name['JenkinsJob'] = _JENKINSJOB
+DESCRIPTOR.message_types_by_name['CastBuild'] = _CASTBUILD
+DESCRIPTOR.message_types_by_name['BuildDetail'] = _BUILDDETAIL
+DESCRIPTOR.message_types_by_name['BuildDetails'] = _BUILDDETAILS
+
+BuildRule = _reflection.GeneratedProtocolMessageType('BuildRule', (_message.Message,), dict(
+
+  PropertyOverridesEntry = _reflection.GeneratedProtocolMessageType('PropertyOverridesEntry', (_message.Message,), dict(
+    DESCRIPTOR = _BUILDRULE_PROPERTYOVERRIDESENTRY,
+    __module__ = 'build_config_pb2'
+    # @@protoc_insertion_point(class_scope:build_config.BuildRule.PropertyOverridesEntry)
+    ))
+  ,
+  DESCRIPTOR = _BUILDRULE,
+  __module__ = 'build_config_pb2'
+  # @@protoc_insertion_point(class_scope:build_config.BuildRule)
+  ))
+_sym_db.RegisterMessage(BuildRule)
+_sym_db.RegisterMessage(BuildRule.PropertyOverridesEntry)
+
+PropertyValue = _reflection.GeneratedProtocolMessageType('PropertyValue', (_message.Message,), dict(
+  DESCRIPTOR = _PROPERTYVALUE,
+  __module__ = 'build_config_pb2'
+  # @@protoc_insertion_point(class_scope:build_config.PropertyValue)
+  ))
+_sym_db.RegisterMessage(PropertyValue)
+
+BuildSet = _reflection.GeneratedProtocolMessageType('BuildSet', (_message.Message,), dict(
+  DESCRIPTOR = _BUILDSET,
+  __module__ = 'build_config_pb2'
+  # @@protoc_insertion_point(class_scope:build_config.BuildSet)
+  ))
+_sym_db.RegisterMessage(BuildSet)
+
+BuildRules = _reflection.GeneratedProtocolMessageType('BuildRules', (_message.Message,), dict(
+
+  BuildSetsEntry = _reflection.GeneratedProtocolMessageType('BuildSetsEntry', (_message.Message,), dict(
+    DESCRIPTOR = _BUILDRULES_BUILDSETSENTRY,
+    __module__ = 'build_config_pb2'
+    # @@protoc_insertion_point(class_scope:build_config.BuildRules.BuildSetsEntry)
+    ))
+  ,
+  DESCRIPTOR = _BUILDRULES,
+  __module__ = 'build_config_pb2'
+  # @@protoc_insertion_point(class_scope:build_config.BuildRules)
+  ))
+_sym_db.RegisterMessage(BuildRules)
+_sym_db.RegisterMessage(BuildRules.BuildSetsEntry)
+
+JenkinsJob = _reflection.GeneratedProtocolMessageType('JenkinsJob', (_message.Message,), dict(
+  DESCRIPTOR = _JENKINSJOB,
+  __module__ = 'build_config_pb2'
+  # @@protoc_insertion_point(class_scope:build_config.JenkinsJob)
+  ))
+_sym_db.RegisterMessage(JenkinsJob)
+
+CastBuild = _reflection.GeneratedProtocolMessageType('CastBuild', (_message.Message,), dict(
+  DESCRIPTOR = _CASTBUILD,
+  __module__ = 'build_config_pb2'
+  # @@protoc_insertion_point(class_scope:build_config.CastBuild)
+  ))
+_sym_db.RegisterMessage(CastBuild)
+
+BuildDetail = _reflection.GeneratedProtocolMessageType('BuildDetail', (_message.Message,), dict(
+
+  PropertyOverridesEntry = _reflection.GeneratedProtocolMessageType('PropertyOverridesEntry', (_message.Message,), dict(
+    DESCRIPTOR = _BUILDDETAIL_PROPERTYOVERRIDESENTRY,
+    __module__ = 'build_config_pb2'
+    # @@protoc_insertion_point(class_scope:build_config.BuildDetail.PropertyOverridesEntry)
+    ))
+  ,
+
+  ScheduleRestriction = _reflection.GeneratedProtocolMessageType('ScheduleRestriction', (_message.Message,), dict(
+    DESCRIPTOR = _BUILDDETAIL_SCHEDULERESTRICTION,
+    __module__ = 'build_config_pb2'
+    # @@protoc_insertion_point(class_scope:build_config.BuildDetail.ScheduleRestriction)
+    ))
+  ,
+  DESCRIPTOR = _BUILDDETAIL,
+  __module__ = 'build_config_pb2'
+  # @@protoc_insertion_point(class_scope:build_config.BuildDetail)
+  ))
+_sym_db.RegisterMessage(BuildDetail)
+_sym_db.RegisterMessage(BuildDetail.PropertyOverridesEntry)
+_sym_db.RegisterMessage(BuildDetail.ScheduleRestriction)
+
+BuildDetails = _reflection.GeneratedProtocolMessageType('BuildDetails', (_message.Message,), dict(
+
+  BuildDetailsEntry = _reflection.GeneratedProtocolMessageType('BuildDetailsEntry', (_message.Message,), dict(
+    DESCRIPTOR = _BUILDDETAILS_BUILDDETAILSENTRY,
+    __module__ = 'build_config_pb2'
+    # @@protoc_insertion_point(class_scope:build_config.BuildDetails.BuildDetailsEntry)
+    ))
+  ,
+  DESCRIPTOR = _BUILDDETAILS,
+  __module__ = 'build_config_pb2'
+  # @@protoc_insertion_point(class_scope:build_config.BuildDetails)
+  ))
+_sym_db.RegisterMessage(BuildDetails)
+_sym_db.RegisterMessage(BuildDetails.BuildDetailsEntry)
+
+
+_BUILDRULE_PROPERTYOVERRIDESENTRY.has_options = True
+_BUILDRULE_PROPERTYOVERRIDESENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+_BUILDRULES_BUILDSETSENTRY.has_options = True
+_BUILDRULES_BUILDSETSENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+_BUILDDETAIL_PROPERTYOVERRIDESENTRY.has_options = True
+_BUILDDETAIL_PROPERTYOVERRIDESENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+_BUILDDETAILS_BUILDDETAILSENTRY.has_options = True
+_BUILDDETAILS_BUILDDETAILSENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+# @@protoc_insertion_point(module_scope)
diff --git a/cq/appengine/proto/build_config.proto b/cq/appengine/proto/build_config.proto
new file mode 100644
index 0000000..b8829a0
--- /dev/null
+++ b/cq/appengine/proto/build_config.proto
@@ -0,0 +1,115 @@
+syntax = "proto2";
+
+package build_config;
+
+message BuildRule {
+  reserved 2;
+
+  // The Gerrit/Git-on-Borg hostname (e.g. eureka-internal)
+  optional string gob_host = 1;
+
+  // A regex to match against a branch.
+  optional string branch = 3;
+
+  // A regex to match against a project name.
+  optional string project = 4;
+
+  // The builds to be automatically scheduled when a CL is uploaded.
+  // These builds should be very fast (<5 mins) to run.
+  repeated string presubmit_builds = 5;
+
+  // The builds to be run when a user requests CQ+1 or CQ+2.
+  // These builds can be slower to run (5-30mins).
+  repeated string builds = 6;
+
+  // A name of a set of builds to be included.
+  repeated string build_sets = 7;
+
+  // Map of property overrides for the project level. It should consist of
+  // property -> override value. For example gclient_root: src.
+  map<string, PropertyValue> property_overrides = 8;
+
+  // List of regex file patterns to run this build against.
+  // If none specified will not check the files.
+  repeated string file_whitelist = 9;
+}
+
+message PropertyValue {
+  oneof value {
+    string string = 1;
+    bool bool = 2;
+  }
+}
+
+message BuildSet {
+  // List of builds in this set.
+  repeated string builds = 1;
+}
+
+message BuildRules {
+  // List of BuildRules.
+  repeated BuildRule build_rules = 1;
+
+  // Map build set name -> List of builds
+  map<string, BuildSet> build_sets = 2;
+}
+
+message JenkinsJob {
+  // The name of the job which should be scheduled in the Jenkins backend.
+  optional string job_name = 1;
+}
+
+message CastBuild {
+  // The name of the Cast CQ recipe to run for this build.
+  optional string recipe = 1;
+  // Override the name provided in the key and pass this builder_name into the
+  // recipe.
+  optional string builder_name = 2;
+}
+
+message BuildDetail {
+  reserved 2, 4, 6, 8, 9, 10, 11, 13, 14, 15;
+
+  // Whether this build is required to pass for a Verified+1.
+  optional bool required = 1 [default = true];
+
+  // Branches that this build is disabled on.
+  // Use the special value "*" to disable on all branches.
+  repeated string disabled_branches = 3;
+
+  // Map of property overrides for the project level. It should consist of
+  // property -> override value. For example gclient_root: src.
+  map<string, PropertyValue> property_overrides = 5;
+
+  message ScheduleRestriction {
+    // The minimum required branch number that this build is supported on.
+    optional string min_branch = 1;
+    // The first branch that this build is not supported on.
+    optional string max_branch = 2;
+    // If this build should only be scheduled on release branches. A release
+    // branch is defined as any branch matching the pattern \d\.\d+[_a-z]*.
+    // Note that this and master_only should never both be true.
+    optional bool release_only = 3 [default = false];
+    // If this build should only be schedule on master branches. Note that
+    // this and release_only should never both be true.
+    optional bool master_only = 4 [default = false];
+  }
+  // The restrictions used to schedule builds. These rules will be applied
+  // first, followed by explicit branch restrictions in disabled_branches.
+  optional ScheduleRestriction schedule = 7;
+
+  // Required field - please populate this with the build OWNER
+  optional string owner = 12;
+
+  oneof config {
+    // The configuration for the cast build to schedule for this build.
+    CastBuild cast_build = 16;
+    // The configuration for the jenkins job to schedule for this build.
+    JenkinsJob jenkins_job = 17;
+  }
+}
+
+message BuildDetails {
+  // Map of build_name -> BuildDetail
+  map<string, BuildDetail> build_details = 1;
+}
diff --git a/cq/landmines/README.md b/cq/landmines/README.md
new file mode 100644
index 0000000..44adf33
--- /dev/null
+++ b/cq/landmines/README.md
@@ -0,0 +1,44 @@
+Landmines let anyone force the CQ build slaves to clean their out directories.
+
+## Using Landmines
+If you want to use a landmine to trigger the bots to clean themselves,
+then go to continuous-tests/cq/landmines (the directory this file is in) and
+bump the number of the landmine file.
+For instance, if there is currently a 'landmine.6' file, create 'landmine.7'.
+The CQ will only care about the landmine with the largest number, regardless
+of how many landmine files there are.
+
+In the landmine file, add a quick note explaining why a landmine is necessary,
+and add a bug number if there is one. This part is purely documentation, but
+it is nice to have.
+
+Then, commit your change with the new/updated landmine file.
+
+The next time the CQ runs, the builder then checks if
+~/landmines/landmine.<number>/<manifest_branch> exists.
+If it already exists, then the builder continues normally.
+If it does not exists, the builder runs:
+   `make clean` && `touch ~/landmines/landmine.<number>/<manifest_branch>`
+This cleans out the $ROOT/out/ directory and all $ROOT/chromium/src/out_*
+directories, and then records that the landmine has been processed.
+
+## Landmine File Format
+A landmine file must be located inside continous-tests/cq/landmines
+A landmine file must have a name of the format: landmine.<number>
+Examples:
+  continuous-tests/cq/landmines/landmine.1
+  continuous-tests/cq/landmines/landmine.23
+  continuous-tests/cq/landmines/landmine.9999
+
+## Landmine File Contents
+The contents of a landmine file do not affect the behaviour of a landmine
+in anyway. However, it is a convenient place to document why a landmine
+was needed. A short description and a bug # are usually good to include.
+
+## Landmine History
+Each slave keeps track of which landmines it has already seen in its
+own ~/landmines directory. Each time a slave sees a new landmine,
+it adds a ~/landmines/<landmine>/<manifest_branch> file to the folder,
+to indicate it saw <landmine> and triggered a clean. Since the CQ slaves keep
+separate checkouts for each manifest branch, the history needs to include
+<manifest_branch>.
diff --git a/cq/landmines/landmine.10 b/cq/landmines/landmine.10
new file mode 100644
index 0000000..4a0e1e6
--- /dev/null
+++ b/cq/landmines/landmine.10
@@ -0,0 +1,6 @@
+b/68346539
+
+Trying to land Chromium roll that includes this CL:
+https://chromium-review.googlesource.com/c/chromium/src/+/729047
+
+Note, it needed a landmine upstream to land.  So do we :)
diff --git a/cq/landmines/landmine.11 b/cq/landmines/landmine.11
new file mode 100644
index 0000000..5278f2f
--- /dev/null
+++ b/cq/landmines/landmine.11
@@ -0,0 +1,4 @@
+I moved some code around in this CL:
+https://eureka-internal-review.git.corp.google.com/c/vendor/amlogic/+/127186
+
+And need to clean up CQ from stale dependencies
diff --git a/cq/landmines/landmine.12 b/cq/landmines/landmine.12
new file mode 100644
index 0000000..eeb667c
--- /dev/null
+++ b/cq/landmines/landmine.12
@@ -0,0 +1,3 @@
+CQ failing, and locally a clean build fixes the failure.
+
+Attempting to fix in CQ by adding this landmine.
diff --git a/cq/landmines/landmine.13 b/cq/landmines/landmine.13
new file mode 100644
index 0000000..d8c1f65
--- /dev/null
+++ b/cq/landmines/landmine.13
@@ -0,0 +1,4 @@
+Chromium's build_time_unittests check that the incremental build is
+no more than 45 days old. We need to do a fresh build in order for 
+the latest merge to pass these tests: b/72118853
+
diff --git a/cq/landmines/landmine.14 b/cq/landmines/landmine.14
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cq/landmines/landmine.14
diff --git a/cq/landmines/landmine.15 b/cq/landmines/landmine.15
new file mode 100644
index 0000000..d8c1f65
--- /dev/null
+++ b/cq/landmines/landmine.15
@@ -0,0 +1,4 @@
+Chromium's build_time_unittests check that the incremental build is
+no more than 45 days old. We need to do a fresh build in order for 
+the latest merge to pass these tests: b/72118853
+
diff --git a/cq/landmines/landmine.16 b/cq/landmines/landmine.16
new file mode 100644
index 0000000..d63a57b
--- /dev/null
+++ b/cq/landmines/landmine.16
@@ -0,0 +1,4 @@
+Chromium's build_time_unittests check that the incremental build is
+no more than 45 days old. We need to do a fresh build in order for 
+the latest merge to pass these tests: b/76204844
+
diff --git a/cq/landmines/landmine.17 b/cq/landmines/landmine.17
new file mode 100644
index 0000000..69095b6
--- /dev/null
+++ b/cq/landmines/landmine.17
@@ -0,0 +1,9 @@
+The following library repos are updated for following reasons
+- tracking new remote upstream
+- cloned to external/gwifi as they are only used by gWifi SDK
+
+The incremental build cannot handle the move the repos and intermediate
+build results for the deprecated library checkouts broken the build,
+thus need a fresh build.
+b/72319911
+go/gwifi-deps-castos
diff --git a/cq/landmines/landmine.18 b/cq/landmines/landmine.18
new file mode 100644
index 0000000..0343551
--- /dev/null
+++ b/cq/landmines/landmine.18
@@ -0,0 +1,4 @@
+Chromium's build_time_unittests check that the incremental build is
+no more than 45 days old. We need to do a fresh build in order for
+the latest merge to pass these tests: b/72157246
+
diff --git a/cq/landmines/landmine.19 b/cq/landmines/landmine.19
new file mode 100644
index 0000000..b2042ce
--- /dev/null
+++ b/cq/landmines/landmine.19
@@ -0,0 +1,7 @@
+b/79208340
+
+I moved the ChromecastConfigAndroid implementation to internal.
+Therefore the path of jni file is updated, and CQ need to clean its out directory.
+
+https://eureka-internal-review.git.corp.google.com/c/chromecast/internal/+/175156
+https://eureka-internal-review.git.corp.google.com/c/chromium/src/+/176180
diff --git a/cq/landmines/landmine.2 b/cq/landmines/landmine.2
new file mode 100644
index 0000000..b2be5f0
--- /dev/null
+++ b/cq/landmines/landmine.2
@@ -0,0 +1,2 @@
+Deploying a change to the JDK version requires clearing out old build
+artifacts. Change is in cl/120723305.
diff --git a/cq/landmines/landmine.20 b/cq/landmines/landmine.20
new file mode 100644
index 0000000..da800a9
--- /dev/null
+++ b/cq/landmines/landmine.20
@@ -0,0 +1 @@
+Need to rebuild waves after https://eureka-partner-review.googlesource.com/c/vendor/waves/+/99308
diff --git a/cq/landmines/landmine.21 b/cq/landmines/landmine.21
new file mode 100644
index 0000000..d01ca16
--- /dev/null
+++ b/cq/landmines/landmine.21
@@ -0,0 +1,19 @@
+b/79208340
+
+Move the ChromecastConfigAndroid implementation to internal.
+Since the path of jni file is updated, CQ need to clean its out directory.
+
+this landmine is used for branch master, branch 1.34 and branch 1.35.
+
+for master:
+https://eureka-internal-review.git.corp.google.com/c/chromecast/internal/+/175156
+https://chromium-review.git.corp.google.com/c/chromium/src/+/1114227
+
+
+for 1.34:
+https://eureka-internal-review.git.corp.google.com/c/chromecast/internal/+/184646
+https://eureka-internal-review.git.corp.google.com/c/chromium/src/+/184642
+
+for 1.35:
+https://eureka-internal-review.git.corp.google.com/c/chromecast/internal/+/192915
+https://eureka-internal-review.git.corp.google.com/c/chromium/src/+/192913
diff --git a/cq/landmines/landmine.22 b/cq/landmines/landmine.22
new file mode 100644
index 0000000..3417cc4
--- /dev/null
+++ b/cq/landmines/landmine.22
@@ -0,0 +1 @@
+Clean the cq for the master branch.
diff --git a/cq/landmines/landmine.23 b/cq/landmines/landmine.23
new file mode 100644
index 0000000..2b25c07
--- /dev/null
+++ b/cq/landmines/landmine.23
@@ -0,0 +1,8 @@
+b/79208340
+
+Move the ChromecastConfigAndroid implementation to internal.
+Since the path of jni file is updated, CQ need to clean its out directory.
+(this landmine is used for the branch 1.36.)
+
+https://eureka-internal-review.git.corp.google.com/c/chromecast/internal/+/199639
+https://eureka-internal-review.git.corp.google.com/c/chromium/src/+/199621
diff --git a/cq/landmines/landmine.24 b/cq/landmines/landmine.24
new file mode 100644
index 0000000..17ddec7
--- /dev/null
+++ b/cq/landmines/landmine.24
@@ -0,0 +1,3 @@
+Clear out dependency cycle caused by old __build_config artifacts.
+
+b/119278504#comment19
diff --git a/cq/landmines/landmine.25 b/cq/landmines/landmine.25
new file mode 100644
index 0000000..a44a9f2
--- /dev/null
+++ b/cq/landmines/landmine.25
@@ -0,0 +1,13 @@
+Need a fresh build to uprev gwifi dependency libs
+
+The following libraries have been deprecated from
+external/gwifi
+- external/gwifi/libbrillo
+- external/gwifi/shill
+
+They have been moved to external/gwifi/platform2
+during the uprev due to the cros upstream change.
+
+b/121276961
+
+
diff --git a/cq/landmines/landmine.26 b/cq/landmines/landmine.26
new file mode 100644
index 0000000..3210726
--- /dev/null
+++ b/cq/landmines/landmine.26
@@ -0,0 +1,4 @@
+Need a fresh build to clean SELinux on Estelle
+
+b/122370575
+
diff --git a/cq/landmines/landmine.27 b/cq/landmines/landmine.27
new file mode 100644
index 0000000..ed4fbaf
--- /dev/null
+++ b/cq/landmines/landmine.27
@@ -0,0 +1,7 @@
+Need a fresh build to clean SELinux on Estelle
+
+Incremental Estelle build was incompartible with all other builds.
+
+Previous solution has been reverted in http://go/eureka-review/229658
+
+b/122370575
\ No newline at end of file
diff --git a/cq/landmines/landmine.28 b/cq/landmines/landmine.28
new file mode 100644
index 0000000..285bb09
--- /dev/null
+++ b/cq/landmines/landmine.28
@@ -0,0 +1,3 @@
+Drop a landmine to fix a cyclical dependency after a GN refactor
+
+crbug.com/934404
diff --git a/cq/landmines/landmine.29 b/cq/landmines/landmine.29
new file mode 100644
index 0000000..1ace07c
--- /dev/null
+++ b/cq/landmines/landmine.29
@@ -0,0 +1,4 @@
+Drop a landmine to fix build failures due to stale makefile path
+
+https://eureka-internal-review.git.corp.google.com/c/eureka/+/254787
+
diff --git a/cq/landmines/landmine.3 b/cq/landmines/landmine.3
new file mode 100644
index 0000000..0b3e502
--- /dev/null
+++ b/cq/landmines/landmine.3
@@ -0,0 +1 @@
+Clean the slaves after a bad push has put the incremental builds into a bad state (b/29466529).
\ No newline at end of file
diff --git a/cq/landmines/landmine.30 b/cq/landmines/landmine.30
new file mode 100644
index 0000000..56ebf35
--- /dev/null
+++ b/cq/landmines/landmine.30
@@ -0,0 +1,9 @@
+Drop a landmine to fix build failures due to incremental builds
+
+Incremental builds are failing to update import_includes when
+adding a new dependent library to LOCAL_STATIC_LIBRARIES.
+
+https://nest-internal-review.googlesource.com/c/platform/nlnetworktools/+/58364
+
+https://b.corp.google.com/issues/132905702 (related bug)
+
diff --git a/cq/landmines/landmine.31 b/cq/landmines/landmine.31
new file mode 100644
index 0000000..2c85576
--- /dev/null
+++ b/cq/landmines/landmine.31
@@ -0,0 +1,5 @@
+Drop a landmine to fix build failures in arm_androidtv_debug builds
+
+Incremental builds in CQ are failing but with ~65% failure rate when CB builds are green.
+
+http://b/134248243 (related bug)
diff --git a/cq/landmines/landmine.32 b/cq/landmines/landmine.32
new file mode 100644
index 0000000..a88eb6d
--- /dev/null
+++ b/cq/landmines/landmine.32
@@ -0,0 +1,10 @@
+Drop a landmine to fix build failures due to incremental builds
+
+Incremental builds are failing to update import_includes when
+adding a new dependent library to LOCAL_STATIC_LIBRARIES.
+
+https://nest-internal-review.googlesource.com/c/platform/nlnetworktools/+/64805
+(Note that this is mostly the same as landmine.30, but used for cherry-picking changes into 1.39n branch)
+
+https://b.corp.google.com/issues/132905702 (related bug)
+
diff --git a/cq/landmines/landmine.33 b/cq/landmines/landmine.33
new file mode 100644
index 0000000..0dc9586
--- /dev/null
+++ b/cq/landmines/landmine.33
@@ -0,0 +1,7 @@
+Drop a landmine to fix build failures due to incremental builds
+
+Incremental builds are failing to correctly configure Weave builds.
+
+
+https://nest-internal-review.googlesource.com/c/platform/weave/+/66378
+
diff --git a/cq/landmines/landmine.34 b/cq/landmines/landmine.34
new file mode 100644
index 0000000..cec8862
--- /dev/null
+++ b/cq/landmines/landmine.34
@@ -0,0 +1,5 @@
+Drop a landmine to fix build failures due to incremental builds
+
+Incremental builds are failing due to camera-common refactor.
+
+https://eureka-internal-review.git.corp.google.com/c/vendor/amlogic/+/290199
diff --git a/cq/landmines/landmine.35 b/cq/landmines/landmine.35
new file mode 100644
index 0000000..b9e5fdd
--- /dev/null
+++ b/cq/landmines/landmine.35
@@ -0,0 +1,11 @@
+Force a clean build for tensorflow uprev.
+
+The following libraries have updated Android.mk in this uprev:
+- libtflite
+https://eureka-internal-review.git.corp.google.com/c/platform/external/tensorflow/+/291899
+- libtextclassifier_hash
+https://eureka-internal-review.git.corp.google.com/c/platform/external/libtextclassifier/+/291918
+- libflatbufferutil
+https://eureka-internal-review.git.corp.google.com/c/platform/external/flatbuffers/+/291977
+
+b/137051697
diff --git a/cq/landmines/landmine.36 b/cq/landmines/landmine.36
new file mode 100644
index 0000000..3ad4b32
--- /dev/null
+++ b/cq/landmines/landmine.36
@@ -0,0 +1,4 @@
+Force a clean build for tensorflow uprev 2.
+
+Tensorflow uprev was partially reverted due to broken build (b/137600157), after
+relanding we need another clean build.
diff --git a/cq/landmines/landmine.37 b/cq/landmines/landmine.37
new file mode 100644
index 0000000..5a6b6f4
--- /dev/null
+++ b/cq/landmines/landmine.37
@@ -0,0 +1 @@
+Force a clean build to fix valens-eng.
\ No newline at end of file
diff --git a/cq/landmines/landmine.38 b/cq/landmines/landmine.38
new file mode 100644
index 0000000..a82b6e5
--- /dev/null
+++ b/cq/landmines/landmine.38
@@ -0,0 +1,6 @@
+Drop a landmine to fix build failures due to incremental builds
+
+Incremental builds are failing due to GQ/NQ system image oversized caused by
+leftover ML modules.
+
+https://eureka-internal-review.git.corp.google.com/c/vendor/amlogic/+/315581
diff --git a/cq/landmines/landmine.39 b/cq/landmines/landmine.39
new file mode 100644
index 0000000..aaa7fd6
--- /dev/null
+++ b/cq/landmines/landmine.39
@@ -0,0 +1,3 @@
+Clean build to eliminate large artifacts stored on CQ slaves. For details see
+b/142762112.
+
diff --git a/cq/landmines/landmine.4 b/cq/landmines/landmine.4
new file mode 100644
index 0000000..5b5fe3e
--- /dev/null
+++ b/cq/landmines/landmine.4
@@ -0,0 +1,2 @@
+Update of Marvell SDK to ver 68051 for steak requires clearing ./out
+for incremental builds.
diff --git a/cq/landmines/landmine.40 b/cq/landmines/landmine.40
new file mode 100644
index 0000000..8b4122d
--- /dev/null
+++ b/cq/landmines/landmine.40
@@ -0,0 +1,2 @@
+Force rebuild for switching prince userspace builds from 32-bit to 64-bit.
+See b/140242422 for more details.
diff --git a/cq/landmines/landmine.41 b/cq/landmines/landmine.41
new file mode 100644
index 0000000..dc7c2c5
--- /dev/null
+++ b/cq/landmines/landmine.41
@@ -0,0 +1,2 @@
+Force rebuild to clean up build errors caused by v4l2_capture refactoring
+b/144132956
diff --git a/cq/landmines/landmine.42 b/cq/landmines/landmine.42
new file mode 100644
index 0000000..4845c7c
--- /dev/null
+++ b/cq/landmines/landmine.42
@@ -0,0 +1,2 @@
+Added NDEBUG flag for eng builds for libchrome and its dependents. Changing
+flag doesn't rebuild the module, hence this landmine.
diff --git a/cq/landmines/landmine.43 b/cq/landmines/landmine.43
new file mode 100644
index 0000000..549ff65
--- /dev/null
+++ b/cq/landmines/landmine.43
@@ -0,0 +1 @@
+DCHECK_ALWAYS_ON is enabled on eng build. Changing compile flag doesn't rebuild the module, hence this landmine.
diff --git a/cq/landmines/landmine.44 b/cq/landmines/landmine.44
new file mode 100644
index 0000000..34531e7
--- /dev/null
+++ b/cq/landmines/landmine.44
@@ -0,0 +1,2 @@
+Clean build required to update exported includes change caused
+by update_metadata-protos being added in eureka-internal/338022
diff --git a/cq/landmines/landmine.45 b/cq/landmines/landmine.45
new file mode 100644
index 0000000..3a69249
--- /dev/null
+++ b/cq/landmines/landmine.45
@@ -0,0 +1,2 @@
+Clean build required to update weave quantity type that was added for
+b/145148808 in http://go/nest-review/109016
diff --git a/cq/landmines/landmine.46 b/cq/landmines/landmine.46
new file mode 100644
index 0000000..aeb64a5
--- /dev/null
+++ b/cq/landmines/landmine.46
@@ -0,0 +1,2 @@
+Clean build to attempt to clean up flakes caused by phoenix-schema
+b/150692335
\ No newline at end of file
diff --git a/cq/landmines/landmine.47 b/cq/landmines/landmine.47
new file mode 100644
index 0000000..2d88dda
--- /dev/null
+++ b/cq/landmines/landmine.47
@@ -0,0 +1,2 @@
+Clean build to attempt to resolve linker errors in ml_framework tryjobs.
+b/149926211
diff --git a/cq/landmines/landmine.48 b/cq/landmines/landmine.48
new file mode 100644
index 0000000..d43727a
--- /dev/null
+++ b/cq/landmines/landmine.48
@@ -0,0 +1,2 @@
+Clean build to attempt to resolve linker errors in nldaemon.
+b/155189910
diff --git a/cq/landmines/landmine.49 b/cq/landmines/landmine.49
new file mode 100644
index 0000000..59dcf21
--- /dev/null
+++ b/cq/landmines/landmine.49
@@ -0,0 +1,4 @@
+Incremental build is failing due to this bug: b/157750636
+
+The /root/lib/teetz symbolic link needs to be removed before
+we can use it as a directory.
diff --git a/cq/landmines/landmine.5 b/cq/landmines/landmine.5
new file mode 100644
index 0000000..eb11aeb
--- /dev/null
+++ b/cq/landmines/landmine.5
@@ -0,0 +1,5 @@
+Disks are filling up out of control. Pass a landmine to reclaim some space
+in a safe way.
+
+b/31319903
+b/31318818
diff --git a/cq/landmines/landmine.50 b/cq/landmines/landmine.50
new file mode 100644
index 0000000..8254f26
--- /dev/null
+++ b/cq/landmines/landmine.50
@@ -0,0 +1,3 @@
+Incremental build is failing due to this bug: b/158347254
+
+Cleanup unused source files by previous Android.mak change
diff --git a/cq/landmines/landmine.51 b/cq/landmines/landmine.51
new file mode 100644
index 0000000..b1d6579
--- /dev/null
+++ b/cq/landmines/landmine.51
@@ -0,0 +1 @@
+Clean build after estelle-user size decreases. b/159068553
\ No newline at end of file
diff --git a/cq/landmines/landmine.52 b/cq/landmines/landmine.52
new file mode 100644
index 0000000..42f99d1
--- /dev/null
+++ b/cq/landmines/landmine.52
@@ -0,0 +1 @@
+Clean build for mushroom ota oversize failure in CQ.
diff --git a/cq/landmines/landmine.53 b/cq/landmines/landmine.53
new file mode 100644
index 0000000..300150f
--- /dev/null
+++ b/cq/landmines/landmine.53
@@ -0,0 +1,2 @@
+Create landmine to fix incremental build failure caused by TDK restructure
+Bug:158269536
diff --git a/cq/landmines/landmine.54 b/cq/landmines/landmine.54
new file mode 100644
index 0000000..1069de8
--- /dev/null
+++ b/cq/landmines/landmine.54
@@ -0,0 +1,2 @@
+Create a landmine to avoid issue with oemcrypto_tz
+Bug:161850133
diff --git a/cq/landmines/landmine.55 b/cq/landmines/landmine.55
new file mode 100644
index 0000000..0ee98b6
--- /dev/null
+++ b/cq/landmines/landmine.55
@@ -0,0 +1,2 @@
+Create a landmine to avoid issue with waves header
+Bug:162108477
diff --git a/cq/landmines/landmine.56 b/cq/landmines/landmine.56
new file mode 100644
index 0000000..80b2ae0
--- /dev/null
+++ b/cq/landmines/landmine.56
@@ -0,0 +1 @@
+Landmine to clean incremental build issue on 1.50 and 1.52
\ No newline at end of file
diff --git a/cq/landmines/landmine.57 b/cq/landmines/landmine.57
new file mode 100644
index 0000000..aae5e8b
--- /dev/null
+++ b/cq/landmines/landmine.57
@@ -0,0 +1,2 @@
+Landmine to force clean build with openssl library switch
+b/150403073
diff --git a/cq/landmines/landmine.58 b/cq/landmines/landmine.58
new file mode 100644
index 0000000..82e96d4
--- /dev/null
+++ b/cq/landmines/landmine.58
@@ -0,0 +1,2 @@
+Landmine to force clean of grpc since we are using a different repo.
+b/132440534
diff --git a/cq/landmines/landmine.59 b/cq/landmines/landmine.59
new file mode 100644
index 0000000..8fef18e
--- /dev/null
+++ b/cq/landmines/landmine.59
@@ -0,0 +1,8 @@
+Landmine to force clean of vento-eng and vento-user intermediate
+build results.
+
+vento-eng and vento-user were both enabled for a period, as a
+result, incompatible intermediate eng/user build results caused
+intermittent CQ failure
+
+b/174605330
diff --git a/cq/landmines/landmine.6 b/cq/landmines/landmine.6
new file mode 100644
index 0000000..0c47dd6
--- /dev/null
+++ b/cq/landmines/landmine.6
@@ -0,0 +1,12 @@
+The cast_test_lists template is not giving ninja all the information it
+needs. A CL went through that added a directory layer (essentially renaming
+a file), but doing `ninja cast_test_lists` was not picking up the change. This
+resulted in running outdated binaries against a checkout, which caused tests
+to fail because the runtime datafile they need had moved.
+
+The CL in question (+2 Depends-On):
+https://eureka-internal-review.git.corp.google.com/c/63729/
+
+This landmine will clear the old test binaries so the new ones will get
+built correctly. The cast_test_list template will be investigated to see
+if it can pass the right dependency info to ninja.
diff --git a/cq/landmines/landmine.60 b/cq/landmines/landmine.60
new file mode 100644
index 0000000..821e445
--- /dev/null
+++ b/cq/landmines/landmine.60
@@ -0,0 +1,6 @@
+Landmine to force clean for all CQ targets that depend on ML framework.
+
+A proto change made in that repo causes a dependency cycle at build time if
+using an old build environment with old versions of the proto libs.
+
+b/177479085
diff --git a/cq/landmines/landmine.61 b/cq/landmines/landmine.61
new file mode 100644
index 0000000..dec8e95
--- /dev/null
+++ b/cq/landmines/landmine.61
@@ -0,0 +1,3 @@
+Landmine to clean out old bad phoenix-schema virtual environments
+
+b/180013158
diff --git a/cq/landmines/landmine.62 b/cq/landmines/landmine.62
new file mode 100644
index 0000000..318cd34
--- /dev/null
+++ b/cq/landmines/landmine.62
@@ -0,0 +1,3 @@
+Force clean build to rebuild spencer image with new partition table
+
+b/174071314
diff --git a/cq/landmines/landmine.63 b/cq/landmines/landmine.63
new file mode 100644
index 0000000..6ca6f0a
--- /dev/null
+++ b/cq/landmines/landmine.63
@@ -0,0 +1,3 @@
+Force clean build to rebuild spencer image with new glibc toolchain
+
+b/186795973
diff --git a/cq/landmines/landmine.64 b/cq/landmines/landmine.64
new file mode 100644
index 0000000..2d846a1
--- /dev/null
+++ b/cq/landmines/landmine.64
@@ -0,0 +1,3 @@
+Landmine to clean out old files for libchrome update.
+
+b/146480114
diff --git a/cq/landmines/landmine.65 b/cq/landmines/landmine.65
new file mode 100644
index 0000000..2e73e1b
--- /dev/null
+++ b/cq/landmines/landmine.65
@@ -0,0 +1,5 @@
+Landmine to clean out x86 host files built with DEBUG.
+
+This is causing intermittent failures on CQ.
+
+b/197920849
diff --git a/cq/landmines/landmine.66 b/cq/landmines/landmine.66
new file mode 100644
index 0000000..3f2fb28
--- /dev/null
+++ b/cq/landmines/landmine.66
@@ -0,0 +1,3 @@
+Landmine to clean old ninja results.
+
+b/259689547
\ No newline at end of file
diff --git a/cq/landmines/landmine.67 b/cq/landmines/landmine.67
new file mode 100644
index 0000000..30a5938
--- /dev/null
+++ b/cq/landmines/landmine.67
@@ -0,0 +1,3 @@
+Landmine to clean old toolchain artifacts
+
+b/267195139
diff --git a/cq/landmines/landmine.68 b/cq/landmines/landmine.68
new file mode 100644
index 0000000..bc330ee
--- /dev/null
+++ b/cq/landmines/landmine.68
@@ -0,0 +1,2 @@
+Landmine to clean old toolchain artifacts
+
diff --git a/cq/landmines/landmine.69 b/cq/landmines/landmine.69
new file mode 100644
index 0000000..cbf28eb
--- /dev/null
+++ b/cq/landmines/landmine.69
@@ -0,0 +1,4 @@
+Landmine to clean old toolchain artifacts
+
+b/286241987
+
diff --git a/cq/landmines/landmine.7 b/cq/landmines/landmine.7
new file mode 100644
index 0000000..160fdab
--- /dev/null
+++ b/cq/landmines/landmine.7
@@ -0,0 +1,8 @@
+Some incremental builds got messed up during a transition from gcc -> clang.
+Most builders correctly cleaned with the CleanSpek, but I think some probably
+did a build in between running CQ and the CL landing which resulted in a weird
+build state.
+
+Most likely related to these:
+https://eureka-internal-review.git.corp.google.com/#/c/69398/2/Android.mk
+https://eureka-internal-review.git.corp.google.com/#/c/69399/
diff --git a/cq/landmines/landmine.8 b/cq/landmines/landmine.8
new file mode 100644
index 0000000..1efbc39
--- /dev/null
+++ b/cq/landmines/landmine.8
@@ -0,0 +1,3 @@
+CL below broke incremental builds:
+
+https://eureka-internal-review.git.corp.google.com/#/c/77288/
diff --git a/cq/landmines/landmine.9 b/cq/landmines/landmine.9
new file mode 100644
index 0000000..67004af
--- /dev/null
+++ b/cq/landmines/landmine.9
@@ -0,0 +1,7 @@
+b/66855290
+
+Builders are failing to build libchrome with:
+error: undefined reference to 'base::ThreadRestrictions::AssertIOAllowed()'
+
+Cleaning the builder appears to let it recover, so probably a CL went through
+that messed up the intermediate build somehow.
diff --git a/cq/requirements_recipe.in b/cq/requirements_recipe.in
new file mode 100644
index 0000000..568b800
--- /dev/null
+++ b/cq/requirements_recipe.in
@@ -0,0 +1,70 @@
+#
+# Please update cq/requirements_recipe_test.in in case of changes here
+#
+# Explicit packages
+home-monitoring==0.13.dev0
+google-api-core[grpc]==1.13.0
+google-apitools==0.5.31
+google-cloud-error-reporting==0.30.1
+netifaces==0.10.9
+networkx==2.2
+mock==2.0.0
+ply==3.8
+pyopenssl==19.0.0
+pre-commit==1.17.0
+psutil==5.5.0
+pycrypto==2.6.1
+pygraphviz==1.5
+requests==2.21.0
+wand==0.4.4
+
+# Implicit packages
+asn1crypto==0.24.0        # via cryptography
+aspy.yaml==1.3.0          # via pre-commit
+cachetools==3.1.0         # via google-auth
+certifi==2018.11.29       # via requests
+cffi==1.14.0              # via cryptography
+cfgv==2.0.1               # via pre-commit
+chardet==3.0.4            # via requests
+configparser==3.7.4       # via importlib-metadata
+contextlib2==0.5.5        # via importlib-metadata
+cryptography==2.5         # via pyopenssl
+decorator==4.3.2          # via networkx
+dill==0.2.9               # via google-gax
+enum34==1.1.6             # via grpcio
+fasteners==0.14.1           # via google-apitools
+fluent-logger==0.9.6     # via home-monitoring
+future==0.16.0            # via google-gax
+google-auth==1.6.2        # via google-api-core
+google-cloud-core==1.0.2  # via google-cloud-logging
+google-cloud-logging==1.11.0  # via google-cloud-error-reporting, home-monitoring
+google-gax==0.16.0        # via home-monitoring
+googleapis-common-protos==1.5.6  # via google-api-core
+grpcio==1.30.0            # via google-api-core
+httplib2==0.12.0          # via google-apitools, oauth2client
+identify==1.4.5            # via pre-commit
+idna==2.8                 # via requests
+importlib-metadata==0.19  # via importlib-resources, pre-commit
+importlib-resources==1.0.2 # via pre-commit
+ipaddress==1.0.22         # via cryptography
+monotonic==1.5            # via fasteners
+msgpack==0.6.1	         # via fluent-logger
+nodeenv==1.3.3            # via pre-commit
+oauth2client==4.1.3       # via google-apitools
+pathlib2==2.3.4           # via importlib-metadata, importlib-resources
+ply==3.8                  # via google-gax
+pbr==5.1.2                # via mock
+protobuf==3.6.1          # via google-api-core, googleapis-common-protos
+pyasn1-modules==0.2.4     # via google-auth, oauth2client
+pyasn1==0.4.5             # via oauth2client, pyasn1-modules, rsa
+pycparser==2.19           # via cffi
+pytz==2018.9              # via google-api-core
+pyyaml==5.1.2             # via pre-commit, aspy.yaml
+rsa==4.0                  # via google-auth, oauth2client
+scandir==1.10.0           # via pathlib2
+six==1.12.0               # via fasteners, google-api-core, google-apitools, google-auth, grpcio, oauth2client, protobuf
+toml==0.10.0              # via pre-commit
+typing==3.7.4             # via importlib-resources
+urllib3==1.24.1           # via requests
+virtualenv==16.7.2        # via pre-commit
+zipp==0.5.2               # via importlib-metadata
diff --git a/cq/requirements_recipe.txt b/cq/requirements_recipe.txt
new file mode 100644
index 0000000..6c1960d
--- /dev/null
+++ b/cq/requirements_recipe.txt
@@ -0,0 +1,75 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+#    pip-compile --output-file=cq/requirements_recipe.txt cq/requirements_recipe.in
+#
+
+asn1crypto==0.24.0
+aspy.yaml==1.3.0
+cachetools==3.1.0
+certifi==2018.11.29
+cffi==1.14.0
+cfgv==2.0.1
+chardet==3.0.4
+configparser==3.7.4
+contextlib2==0.5.5
+cryptography==2.5
+decorator==4.3.2
+dill==0.2.9
+enum34==1.1.6
+fasteners==0.14.1
+fluent-logger==0.9.6
+funcsigs==1.0.2           # via mock
+future==0.16.0
+futures==3.3.0            # via google-api-core, grpcio, pre-commit
+google-api-core[grpc]==1.13.0
+google-apitools==0.5.31
+google-auth==1.6.2
+google-cloud-core==1.0.2
+google-cloud-error-reporting==0.30.1
+google-cloud-logging==1.11.0
+google-gax==0.16.0
+googleapis-common-protos==1.5.6
+grpcio==1.30.0
+home-monitoring==0.13.dev0
+httplib2==0.12.0
+identify==1.4.5
+idna==2.8
+importlib-metadata==0.19
+importlib-resources==1.0.2
+ipaddress==1.0.22
+mock==2.0.0
+monotonic==1.5
+msgpack==0.6.1
+netifaces==0.10.9
+networkx==2.2
+nodeenv==1.3.3
+oauth2client==4.1.3
+pathlib2==2.3.4
+pbr==5.1.2
+ply==3.8
+pre-commit==1.17.0
+protobuf==3.6.1
+psutil==5.5.0
+pyasn1-modules==0.2.4
+pyasn1==0.4.5
+pycparser==2.19
+pycrypto==2.6.1
+pygraphviz==1.5
+pyopenssl==19.0.0
+pytz==2018.9
+pyyaml==5.1.2
+requests==2.21.0
+rsa==4.0
+scandir==1.10.0
+six==1.12.0
+toml==0.10.0
+typing==3.7.4
+urllib3==1.24.1
+virtualenv==16.7.2
+wand==0.4.4
+zipp==0.5.2
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/cq/requirements_recipe3.in b/cq/requirements_recipe3.in
new file mode 100644
index 0000000..568b800
--- /dev/null
+++ b/cq/requirements_recipe3.in
@@ -0,0 +1,70 @@
+#
+# Please update cq/requirements_recipe_test.in in case of changes here
+#
+# Explicit packages
+home-monitoring==0.13.dev0
+google-api-core[grpc]==1.13.0
+google-apitools==0.5.31
+google-cloud-error-reporting==0.30.1
+netifaces==0.10.9
+networkx==2.2
+mock==2.0.0
+ply==3.8
+pyopenssl==19.0.0
+pre-commit==1.17.0
+psutil==5.5.0
+pycrypto==2.6.1
+pygraphviz==1.5
+requests==2.21.0
+wand==0.4.4
+
+# Implicit packages
+asn1crypto==0.24.0        # via cryptography
+aspy.yaml==1.3.0          # via pre-commit
+cachetools==3.1.0         # via google-auth
+certifi==2018.11.29       # via requests
+cffi==1.14.0              # via cryptography
+cfgv==2.0.1               # via pre-commit
+chardet==3.0.4            # via requests
+configparser==3.7.4       # via importlib-metadata
+contextlib2==0.5.5        # via importlib-metadata
+cryptography==2.5         # via pyopenssl
+decorator==4.3.2          # via networkx
+dill==0.2.9               # via google-gax
+enum34==1.1.6             # via grpcio
+fasteners==0.14.1           # via google-apitools
+fluent-logger==0.9.6     # via home-monitoring
+future==0.16.0            # via google-gax
+google-auth==1.6.2        # via google-api-core
+google-cloud-core==1.0.2  # via google-cloud-logging
+google-cloud-logging==1.11.0  # via google-cloud-error-reporting, home-monitoring
+google-gax==0.16.0        # via home-monitoring
+googleapis-common-protos==1.5.6  # via google-api-core
+grpcio==1.30.0            # via google-api-core
+httplib2==0.12.0          # via google-apitools, oauth2client
+identify==1.4.5            # via pre-commit
+idna==2.8                 # via requests
+importlib-metadata==0.19  # via importlib-resources, pre-commit
+importlib-resources==1.0.2 # via pre-commit
+ipaddress==1.0.22         # via cryptography
+monotonic==1.5            # via fasteners
+msgpack==0.6.1	         # via fluent-logger
+nodeenv==1.3.3            # via pre-commit
+oauth2client==4.1.3       # via google-apitools
+pathlib2==2.3.4           # via importlib-metadata, importlib-resources
+ply==3.8                  # via google-gax
+pbr==5.1.2                # via mock
+protobuf==3.6.1          # via google-api-core, googleapis-common-protos
+pyasn1-modules==0.2.4     # via google-auth, oauth2client
+pyasn1==0.4.5             # via oauth2client, pyasn1-modules, rsa
+pycparser==2.19           # via cffi
+pytz==2018.9              # via google-api-core
+pyyaml==5.1.2             # via pre-commit, aspy.yaml
+rsa==4.0                  # via google-auth, oauth2client
+scandir==1.10.0           # via pathlib2
+six==1.12.0               # via fasteners, google-api-core, google-apitools, google-auth, grpcio, oauth2client, protobuf
+toml==0.10.0              # via pre-commit
+typing==3.7.4             # via importlib-resources
+urllib3==1.24.1           # via requests
+virtualenv==16.7.2        # via pre-commit
+zipp==0.5.2               # via importlib-metadata
diff --git a/cq/requirements_recipe3.txt b/cq/requirements_recipe3.txt
new file mode 100644
index 0000000..7338081
--- /dev/null
+++ b/cq/requirements_recipe3.txt
@@ -0,0 +1,242 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+#    pip-compile cq/requirements_recipe3.in
+#
+asn1crypto==0.24.0
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   cryptography
+aspy.yaml==1.3.0
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   pre-commit
+cachetools==3.1.0
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-auth
+certifi==2018.11.29
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   requests
+cffi==1.14.0
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   cryptography
+cfgv==2.0.1
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   pre-commit
+chardet==3.0.4
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   requests
+configparser==3.7.4
+    # via -r cq/requirements_recipe3.in
+contextlib2==0.5.5
+    # via -r cq/requirements_recipe3.in
+cryptography==2.5
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   pyopenssl
+decorator==4.3.2
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   networkx
+dill==0.2.9
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-gax
+enum34==1.1.6
+    # via -r cq/requirements_recipe3.in
+fasteners==0.14.1
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-apitools
+fluent-logger==0.9.6
+    # via -r cq/requirements_recipe3.in
+future==0.16.0
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-gax
+google-api-core[grpc]==1.13.0
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-cloud-core
+    #   google-cloud-logging
+google-apitools==0.5.31
+    # via -r cq/requirements_recipe3.in
+google-auth==1.6.2
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-api-core
+    #   google-gax
+google-cloud-core==1.0.2
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-cloud-logging
+google-cloud-error-reporting==0.30.1
+    # via -r cq/requirements_recipe3.in
+google-cloud-logging==1.11.0
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-cloud-error-reporting
+google-gax==0.16.0
+    # via -r cq/requirements_recipe3.in
+googleapis-common-protos==1.5.6
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-api-core
+    #   google-gax
+grpcio==1.30.0
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-api-core
+    #   google-gax
+home-monitoring==0.13.dev0
+    # via -r cq/requirements_recipe3.in
+httplib2==0.12.0
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-apitools
+    #   oauth2client
+identify==1.4.5
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   pre-commit
+idna==2.8
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   requests
+importlib-metadata==0.19
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   pre-commit
+importlib-resources==1.0.2
+    # via -r cq/requirements_recipe3.in
+ipaddress==1.0.22
+    # via -r cq/requirements_recipe3.in
+mock==2.0.0
+    # via -r cq/requirements_recipe3.in
+monotonic==1.5
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   fasteners
+msgpack==0.6.1
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   fluent-logger
+netifaces==0.10.9
+    # via -r cq/requirements_recipe3.in
+networkx==2.2
+    # via -r cq/requirements_recipe3.in
+nodeenv==1.3.3
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   pre-commit
+oauth2client==4.1.3
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-apitools
+pathlib2==2.3.4
+    # via -r cq/requirements_recipe3.in
+pbr==5.1.2
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   mock
+ply==3.8
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-gax
+pre-commit==1.17.0
+    # via -r cq/requirements_recipe3.in
+protobuf==3.6.1
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-api-core
+    #   google-gax
+    #   googleapis-common-protos
+psutil==5.5.0
+    # via -r cq/requirements_recipe3.in
+pyasn1-modules==0.2.4
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-auth
+    #   oauth2client
+pyasn1==0.4.5
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   oauth2client
+    #   pyasn1-modules
+    #   rsa
+pycparser==2.19
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   cffi
+pycrypto==2.6.1
+    # via -r cq/requirements_recipe3.in
+pygraphviz==1.5
+    # via -r cq/requirements_recipe3.in
+pyopenssl==19.0.0
+    # via -r cq/requirements_recipe3.in
+pytz==2018.9
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-api-core
+pyyaml==5.1.2
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   aspy.yaml
+    #   pre-commit
+requests==2.21.0
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-api-core
+    #   google-gax
+rsa==4.0
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   google-auth
+    #   oauth2client
+scandir==1.10.0
+    # via -r cq/requirements_recipe3.in
+six==1.12.0
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   cfgv
+    #   cryptography
+    #   fasteners
+    #   google-api-core
+    #   google-apitools
+    #   google-auth
+    #   grpcio
+    #   mock
+    #   oauth2client
+    #   pathlib2
+    #   pre-commit
+    #   protobuf
+    #   pyopenssl
+toml==0.10.0
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   pre-commit
+typing==3.7.4
+    # via -r cq/requirements_recipe3.in
+urllib3==1.24.1
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   requests
+virtualenv==16.7.2
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   pre-commit
+wand==0.4.4
+    # via -r cq/requirements_recipe3.in
+zipp==0.5.2
+    # via
+    #   -r cq/requirements_recipe3.in
+    #   importlib-metadata
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/cq/scripts/__init__.py b/cq/scripts/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cq/scripts/__init__.py
diff --git a/cq/scripts/helpers/__init__.py b/cq/scripts/helpers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cq/scripts/helpers/__init__.py
diff --git a/cq/scripts/helpers/accelerator_utils.py b/cq/scripts/helpers/accelerator_utils.py
new file mode 100644
index 0000000..fea0a05
--- /dev/null
+++ b/cq/scripts/helpers/accelerator_utils.py
@@ -0,0 +1,6 @@
+from slave.accelerator import no_accelerator
+from slave.accelerator import rbe_accelerator
+
+
+def get_build_accelerator():
+  return rbe_accelerator.Accelerator()
diff --git a/cq/scripts/helpers/branch_utils.py b/cq/scripts/helpers/branch_utils.py
new file mode 100644
index 0000000..c0841e9
--- /dev/null
+++ b/cq/scripts/helpers/branch_utils.py
@@ -0,0 +1,80 @@
+"""Utilities related to the Cast branching scheme."""
+
+from __future__ import absolute_import
+import re
+
+RELEASE_BRANCH_PATTERN = r'(?P<major>\d+)\.(?P<minor>\d+)(?P<version>.+)?'
+# See go/nest-tv-branch-strategy.
+ANDROID_TV_RELEASE_BRANCH_PATTERN = (r'^android(\d+)-(\d+\.\d+)-(.+)'
+                                     r'-(factory|ota)(|\d+)$')
+
+
+def parse_release_branch(branch_name):
+  """Converts string branch_name to tuple (int major, int minor, version)."""
+  match = re.match(RELEASE_BRANCH_PATTERN, branch_name)
+  if not match:
+    raise ValueError('Invalid branch name: {}'.format(branch_name))
+  major = int(match.group('major'))
+  minor = int(match.group('minor'))
+  version = match.group('version')
+  try:
+    version = int(version)
+  except (TypeError, ValueError):
+    pass
+  return major, minor, version
+
+
+def is_release_branch(branch):
+  """Returns True if |branch| matching release branch patterns."""
+  return (branch and
+          (re.match(RELEASE_BRANCH_PATTERN, branch) or
+           re.match(ANDROID_TV_RELEASE_BRANCH_PATTERN, branch)))
+
+
+def is_master_branch(branch):
+  """Returns True if |branch| is a master branch."""
+  return branch == 'master'
+
+
+def is_branch_equal_to_or_later_than(branch, threshold):
+  """Returns True if |branch| is at least as new as |threshold|.
+
+  Returns False if |branch| is older than |threshold|. Returns True if
+  |branch| is newer than |threshold|, matches |threshold|, or is 'master'.
+
+  Args:
+    branch: A Cast branch, of the form X.YY{a-d} or 'master'.
+    threshold: A Cast branch, of the form X.YY{a-d} or 'master'.
+
+  Returns:
+    True if |branch| is at least as new as |threshold|.
+
+  Raises:
+    ValueError: if either |branch| or |threshold| is poorly-formed.
+  """
+  if is_master_branch(branch):
+    return True
+  # This means it is a named branch. Usually a bringup branch so we should run
+  # everything.
+  if not is_release_branch(branch):
+    return True
+  # Exclude Android TV release branch from version comparison.
+  if re.match(ANDROID_TV_RELEASE_BRANCH_PATTERN, branch):
+    return True
+  if threshold == 'master':
+    return False
+  if threshold is None:
+    return True
+
+  t_major, t_minor, t_ver = parse_release_branch(threshold)
+  major, minor, ver = parse_release_branch(branch)
+  t_ver = t_ver if t_ver is not None else -1
+  ver = ver if ver is not None else -1
+  # If the version is a string for one of them we need to compare both as
+  # strings.
+  if any(isinstance(v, str) for v in (t_ver, ver)):
+    t_ver = str(t_ver)
+    ver = str(ver)
+  return (major > t_major or
+          (major == t_major and minor > t_minor) or
+          (major == t_major and minor == t_minor and ver >= t_ver))
diff --git a/cq/scripts/helpers/branch_utils_test.py b/cq/scripts/helpers/branch_utils_test.py
new file mode 100644
index 0000000..e340919
--- /dev/null
+++ b/cq/scripts/helpers/branch_utils_test.py
@@ -0,0 +1,127 @@
+"""Tests for branch_utils."""
+
+from __future__ import absolute_import
+import os
+import sys
+import unittest
+
+# Allow imports from cq/scripts folder
+sys.path.insert(
+    0,
+    os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir)))
+from helpers import branch_utils
+
+
+class BranchUtilsTest(unittest.TestCase):
+
+  def testAnyBranchAlwaysLaterThanNone(self):
+    """Test that the master branch is always the latest branch."""
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('master', None))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('0.00', None))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('3.45', None))
+
+  def testMasterBranchAlwaysLatest(self):
+    """Test that the master branch is always the latest branch."""
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('master', 'master'))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('master', '2.34'))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('master', '0.00'))
+    self.assertFalse(
+        branch_utils.is_branch_equal_to_or_later_than('3.45', 'master'))
+    self.assertFalse(
+        branch_utils.is_branch_equal_to_or_later_than('0.00', 'master'))
+
+  def testNamedBranchLatest(self):
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('pi-tv-dev', 'master'))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('pi-tv-dev', '2.34'))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('pi-tv-dev', '0.00'))
+
+  def testAndroidTvBranchLatest(self):
+    self.assertTrue(branch_utils.is_branch_equal_to_or_later_than(
+        'android12-5.4-boreal', None))
+    self.assertTrue(branch_utils.is_branch_equal_to_or_later_than(
+        'android12-5.4-boreal', 'master'))
+    self.assertTrue(branch_utils.is_branch_equal_to_or_later_than(
+        'android12-5.4-boreal', '2.34'))
+    # Android TV release branch.
+    self.assertTrue(branch_utils.is_branch_equal_to_or_later_than(
+        'android12-5.4-boreal-factory', None))
+    self.assertTrue(branch_utils.is_branch_equal_to_or_later_than(
+        'android12-5.4-boreal-factory', 'master'))
+    self.assertTrue(branch_utils.is_branch_equal_to_or_later_than(
+        'android12-5.4-boreal-factory', '2.34'))
+
+  def testBasicBranchComparison(self):
+    """Test that branches compare properly to one another."""
+    self.assertFalse(
+        branch_utils.is_branch_equal_to_or_later_than('1.21', '2.34'))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('2.34', '1.21'))
+    self.assertFalse(
+        branch_utils.is_branch_equal_to_or_later_than('2.1', '11.1'))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('11.1', '2.1'))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('1.21', '1.21'))
+
+  def testBranchesWithSameMajorDifferentMinor(self):
+    """Test that branches with the same major revision compare properly."""
+    self.assertFalse(
+        branch_utils.is_branch_equal_to_or_later_than('1.21', '1.22'))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('1.22', '1.21'))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('1.100', '1.23'))
+    self.assertFalse(
+        branch_utils.is_branch_equal_to_or_later_than('1.23', '1.100'))
+
+  def testBranchesWithSameMajorAndMinorDifferentVersion(self):
+    """Test same major, minor, but with a different verison."""
+    self.assertFalse(
+        branch_utils.is_branch_equal_to_or_later_than('1.21', '1.21a'))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('1.21a', '1.21'))
+    self.assertFalse(
+        branch_utils.is_branch_equal_to_or_later_than('1.21a', '1.21b'))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('1.21b', '1.21a'))
+    self.assertFalse(
+        branch_utils.is_branch_equal_to_or_later_than('1.21d', '1.22'))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('1.22', '1.21d'))
+    self.assertTrue(
+        branch_utils.is_branch_equal_to_or_later_than('1.21d', '1.21d'))
+
+  def testReleaseBranches(self):
+    self.assertFalse(branch_utils.is_release_branch(None))
+    self.assertFalse(branch_utils.is_release_branch(''))
+    self.assertFalse(branch_utils.is_release_branch('master'))
+    self.assertFalse(branch_utils.is_release_branch('estelle'))
+    self.assertFalse(branch_utils.is_release_branch('android12-5.4-boreal'))
+    self.assertTrue(branch_utils.is_release_branch('1.36'))
+    self.assertTrue(branch_utils.is_release_branch('1.36a'))
+    self.assertTrue(branch_utils.is_release_branch('1.36_sdk_estelle'))
+    self.assertTrue(branch_utils.is_release_branch(
+        'android12-5.4-boreal-factory'))
+    self.assertTrue(branch_utils.is_release_branch(
+        'android12-5.4-boreal-ota12'))
+
+  def testMasterBranches(self):
+    self.assertFalse(branch_utils.is_master_branch(None))
+    self.assertFalse(branch_utils.is_master_branch(''))
+    self.assertFalse(branch_utils.is_master_branch('estelle'))
+    self.assertFalse(branch_utils.is_master_branch('1.36'))
+    self.assertFalse(branch_utils.is_master_branch('1.36a'))
+    self.assertFalse(branch_utils.is_master_branch('1.36_sdk_estelle'))
+    self.assertTrue(branch_utils.is_master_branch('master'))
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/cast_shell_utils.py b/cq/scripts/helpers/cast_shell_utils.py
new file mode 100644
index 0000000..edb1377
--- /dev/null
+++ b/cq/scripts/helpers/cast_shell_utils.py
@@ -0,0 +1,266 @@
+"""Utils for running a desktop cast_shell instance."""
+
+from __future__ import absolute_import
+import json
+import logging
+import os
+import shutil
+import socket
+import sys
+import time
+
+# The step property key that stores the cast_shell executable path.
+CAST_SHELL_EXE_PATH = 'cast_shell_exe_path'
+
+_AVAILABILITY_RETRY_WAIT_TIME_SECS = 10
+_AVAILABILITY_WAIT_TIME_SECS = 80
+_CAST_V2_DEBUG_APP_ID = 'C566BECD'
+_CONNECTION_NAMESPACE = 'urn:x-cast:com.google.cast.tp.connection'
+_SETUP_API_ROOT = 'http://{device_ip}:8008/setup/{api}'
+_OZONE_PLATFORM_CAST = 'cast'
+_OZONE_PLATFORM_HEADLESS = 'headless'  # Used for chirp_shell.
+
+# Default cast_shell parameters
+_CAST_V2_PORT = 8009
+_MULTIZONE_PORT = 10001
+_REMOTE_DUBIGGING_PORT = 9222
+_MDNS_MULTICAST_PORT = 5353
+_GCM_SUB_PATH = 'gcm_cast_cq'
+
+_DUMP_LOGCAT_MSG = ('dump of cast_shell logs: \n'
+                    '*** BEGIN CAST_SHELL ***\n'
+                    '%s\n'
+                    '*** END CAST_SHELL ***\n')
+
+
+class CastShellError(Exception):
+  """An error while trying to use cast_shell."""
+
+
+class CastShellTimeoutError(Exception):
+  """A timeout error while trying to use cast_shell."""
+
+
+def start_cast_shell(executor,
+                     test_project_path,
+                     cast_shell_path,
+                     tmp_dir,
+                     enable_cma_media_pipeline=False,
+                     enable_ccs_transport_v2=False,
+                     use_headless_ozone_platform=False,
+                     launch_utility_process=False):
+  """Starts a local x86 cast shell.
+
+  Note: if multiple cast_shell instances are to be started, they must
+  all have unique ports for all the keyword ports specified below and unique
+  HOME and TMPDIR directories.
+
+  Args:
+    executor: Executor to run the subprocess
+    test_project_path: Path to the 'test' project. This will be appened to the
+      sys.path if it is not already there.
+    cast_shell_path: Path the to cast_shell executable
+    tmp_dir: A temporary dir to use as HOME and TMPDIR. If there are multiple
+      cast_shell instances, this should be unique across all instances.
+    enable_cma_media_pipeline: If True, pass in --enable-cma-media-pipeline
+    enable_ccs_transport_v2: If True, pass in --enable-ccs-transport-v2
+    use_headless_ozone_platform: If True, use "headless" as --ozone-platform
+      (required for assistant-enabled cast_shells); otherwise, uses "cast".
+    launch_utility_process: If True, launches the utility process (required to
+      start the assistant debug HTTP server at port 8007).
+
+  Returns:
+    A tuple of (<started subprocess>,
+                <file descriptor of cast_shell output file>,
+                <absolute path of cast_shell output file>).
+    These can be passed to upload_cast_shell_logs() below to store the log
+    files in GCS.
+
+  Raises:
+    CastShellError: if cast_shell does not start correctly.
+    CastShellTimeoutError: if cast_shell takes too long to become available.
+  """
+  if test_project_path not in sys.path:
+    sys.path.append(test_project_path)
+
+  if use_headless_ozone_platform:
+    ozone_platform = _OZONE_PLATFORM_HEADLESS
+  else:
+    ozone_platform = _OZONE_PLATFORM_CAST
+
+  command = [
+      cast_shell_path,
+      '--no-sandbox',
+      '--disable-gpu',
+      '--ignore-gpu-blacklist',
+      '--ozone-platform={}'.format(ozone_platform),
+      '--port-for-cast-v2={}'.format(_CAST_V2_PORT),
+      '--port-for-multizone={}'.format(_MULTIZONE_PORT),
+      '--remote-debugging-port={}'.format(_REMOTE_DUBIGGING_PORT),
+      '--port-for-mdns-multicast={}'.format(_MDNS_MULTICAST_PORT),
+      '--gcm-sub-path="{}"'.format(_GCM_SUB_PATH),
+      '--disable-reboot-on-config-update',
+  ]
+
+  if launch_utility_process:
+    # max-output-volume-dba1m is required when --launch-utility-process
+    # is specified.
+    command.extend(['--launch-utility-process', '--max-output-volume-dba1m=88'])
+
+  if enable_cma_media_pipeline:
+    command.append('--enable-cma-media-pipeline')
+  if enable_ccs_transport_v2:
+    command.append('--enable-ccs-transport-v2')
+
+  # HOME and TMPDIR should be unique across any running cast_shell instances.
+  cast_shell_env = {'HOME': tmp_dir, 'TMPDIR': tmp_dir}
+
+  try:
+    subprocess, log_file_descriptor, log_file_path = (
+        executor.exec_non_blocking_subprocess(command, env=cast_shell_env))
+
+    if subprocess.returncode is not None:
+      raise CastShellError('cast_shell startup failed')
+
+    if not _await_cast_shell_availability():
+      raise CastShellTimeoutError(
+          'cast_shell took too long to become available.')
+
+  except:
+    logging.exception('Unable to start Cast Shell.')
+    _dump_cast_shell_logs(log_file_descriptor, log_file_path)
+    raise
+
+  logging.info('cast_shell started successfully.')
+  return subprocess, log_file_descriptor, log_file_path
+
+
+def _dump_cast_shell_logs(log_file_descriptor, log_file_path):
+  """Dump the cast_shell logcat to the logger."""
+  os.fsync(log_file_descriptor)
+  os.close(log_file_descriptor)
+  with open(log_file_path, 'r') as log_file:
+    logging.info(_DUMP_LOGCAT_MSG, log_file.read())
+
+
+def upload_cast_shell_logs(log_file_descriptor, log_file_path, gcs_root_dir,
+                           sub_dir, upload_file_name):
+  """Move the process log file into GCS folder so that it's uploaded.
+
+  Args:
+    log_file_descriptor: File descriptor of log file to be copied. This should
+      be the descriptor returned by start_cast_shell() above.
+    log_file_path: Absolute path of the log file to be copied. This should be
+      the path returned by start_cast_shell() above.
+    gcs_root_dir: The GCS directory.
+    sub_dir: The sub directory within GCS to upload to.
+    upload_file_name: A string; log file will be renamed and uploaded with this
+      name.
+  """
+  output_dir = os.path.join(gcs_root_dir, sub_dir)
+  if not os.path.exists(output_dir):
+    os.makedirs(output_dir)
+  os.fsync(log_file_descriptor)
+  shutil.copy(log_file_path, os.path.join(output_dir, upload_file_name))
+  logging.info(
+      'cast_shell log will be avilable on GCS in the gs://cast-cq bucket under '
+      '%s for this CL. (See the end of stdio of the "shell" step for the '
+      'specific path. It should be similar to: '
+      'gs://cast-cq/eureka-interal/project/branch/issue/patchset',
+      os.path.join(sub_dir, upload_file_name))
+
+
+def _get_eureka_info(executor, ip_address='localhost', params=None):
+  """Returns the parsed eureka info.
+
+  The //chromecast/internal/setup/eureka_info.h describes the various fields
+  that will be available.
+
+  This assumes that a cast_shell instance is available at |ip_address| (such
+  as from running start_cast_shell above).
+
+  Args:
+    executor: Executor to run the subprocess
+    ip_address: IP address (as a string) of the device to get the info from.
+      (Defaults to 'localhost')
+    params: A list of parameters (as strings) to get from euruka_info. This must
+      be specified and cannot be an empty list. e.g. params=['name',
+      'wifi.ssid', 'setup.stats.num_obtain_ip'] will
+        return {'name': 'devicename',
+                'wifi': {'ssid': 'wifi-ssid-value'},
+                'setup': {'stats': {'num_obtain_ip': 0}}}
+
+  Returns:
+    A dict with the eureka_info information on success, or an empty dict on
+    failure.
+  """
+  api_endpoint = _SETUP_API_ROOT.format(
+      device_ip=ip_address,
+      api='eureka_info?params={}'.format(','.join(params)))
+  logging.info('Making Request to API Endpoint: %s', api_endpoint)
+  returncode, json_result, _ = executor.exec_subprocess(['curl', api_endpoint],
+                                                        quiet=True)
+  if returncode == 0:
+    return json.loads(json_result)
+
+  return {}
+
+
+def get_device_name(executor, ip_address='localhost'):
+  """Returns the name of the device at |ip_address|."""
+  return _get_eureka_info(executor, ip_address, ['name']).get('name')
+
+
+def _await_cast_shell_availability(max_wait_time=_AVAILABILITY_WAIT_TIME_SECS):
+  """Wait's until max_wait_time seconds for cast shell availability.
+
+  Args:
+    max_wait_time: Integer of wait time in seconds.
+
+  Returns:
+    True if cast shell is up and running.
+  """
+  remaining_time = max_wait_time
+  while remaining_time >= 0:
+    time.sleep(_AVAILABILITY_RETRY_WAIT_TIME_SECS)
+    remaining_time -= _AVAILABILITY_RETRY_WAIT_TIME_SECS
+    try:
+      if _connect_cast_shell():
+        return True
+    except (socket.error, AssertionError):
+      logging.exception('Cast shell is not ready, will retry for %d seconds',
+                        remaining_time)
+  return False
+
+
+def _connect_cast_shell():
+  """Returns True if able to to connect and send a message to cast shell.
+
+  Returns:
+    True if able to to connect and send a message to cast shell.
+
+  Raises:
+    Socket.error: if not able to connect to cast shell.
+    AssertionError: if not able to send message to cast shell.
+  """
+
+  # Assumes that test/ is on the PYTHONPATH
+  transport = __import__(
+      'cast.tools.transport',
+      fromlist=[
+          'application', 'application_connection', 'receiver', 'transport'
+      ])
+  receiver = transport.receiver.Receiver('receiver-0', 'localhost')
+  app = transport.application.Application(receiver, _CAST_V2_DEBUG_APP_ID)
+  try:
+    with (transport.application_connection.ApplicationConnection(
+        app, 'sender-1')) as app_conn:
+      app_conn.ConnectToReceiver()
+      message = transport.transport.BuildMessage(
+          'sender-1', 'receiver-0', _CONNECTION_NAMESPACE, b'', binary=True)
+      app_conn.Send(message)
+      app_conn.Status()
+  except transport.socket_helper.SocketError as e:
+    raise socket.error(socket.error(e), sys.exc_info()[2])
+
+  return True
diff --git a/cq/scripts/helpers/cipd_upload_utils.py b/cq/scripts/helpers/cipd_upload_utils.py
new file mode 100644
index 0000000..d15dba5
--- /dev/null
+++ b/cq/scripts/helpers/cipd_upload_utils.py
@@ -0,0 +1,60 @@
+"""Collection of utility functions for uploading CIPD packages.
+
+Note: cipd is included in depot_tools. These utils assume depot_tools has been
+included in $PATH.
+"""
+
+from __future__ import absolute_import
+import logging
+
+
+def UploadCIPDPackage(executor,
+                      pkg_def,
+                      pkg_vars=None,
+                      refs=None,
+                      tags=None,
+                      verification_timeout=None,
+                      service_account_json=None,
+                      dry_run=False):
+  """Uploads a new CIPD package, defined by pkg-def.
+
+  Args:
+    executor: Executes the subprocess.
+    pkg_def: Path to cipd pkg-def .yaml file.
+    pkg_vars: A dict of variables to pass to cipd with -pkg-var.
+    refs: A list of refs to be assigned to the uploaded cipd package.
+    tags: A dict of tags to be assigned to the uploaded cipd package.
+    verification_timeout: Time allotted for cipd verification step ('XXmXXs').
+    service_account_json: Path to service account JSON auth token.
+    dry_run: Boolean, if True skip actual upload.
+
+  Returns:
+    Tuple of (return code, full stdout output, full stderr output).
+  """
+  pkg_vars = pkg_vars or {}
+  refs = refs or []
+  tags = tags or {}
+  cmd = [
+      'cipd', 'create',
+      '-pkg-def', pkg_def]
+  for key in pkg_vars.keys():
+    value = pkg_vars[key]
+    var = ':'.join([str(key), str(value)])
+    cmd += ['-pkg-var', var]
+  for ref in refs:
+    cmd += ['-ref', ref]
+  for key in tags.keys():
+    value = tags[key]
+    tag = ':'.join([str(key), str(value)])
+    cmd += ['-tag', tag]
+
+  if verification_timeout:
+    cmd += ['-verification-timeout', verification_timeout]
+
+  if service_account_json:
+    cmd += ['-service-account-json', service_account_json]
+
+  if dry_run:
+    logging.info('CIPD upload command: %s', cmd)
+    cmd = ['echo'] + cmd
+  return executor.exec_subprocess(cmd)
diff --git a/cq/scripts/helpers/cipd_upload_utils_test.py b/cq/scripts/helpers/cipd_upload_utils_test.py
new file mode 100644
index 0000000..f9f92ff
--- /dev/null
+++ b/cq/scripts/helpers/cipd_upload_utils_test.py
@@ -0,0 +1,50 @@
+"""Test for cipd_upload_utils."""
+import unittest
+import unittest.mock
+
+from cq.scripts.helpers import cipd_upload_utils
+
+
+class CIPDUploadUtilsTest(unittest.TestCase):
+
+  def setUp(self):
+    self._executor = unittest.mock.MagicMock()
+
+  def testUploadCIPDPackage(self):
+    """Verifies simple UploadCIPDPackage call."""
+    pkg_def = '/path/to/pkg.yaml'
+
+    cipd_upload_utils.UploadCIPDPackage(self._executor, pkg_def, dry_run=False)
+
+    self._executor.exec_subprocess.assert_called_once_with([
+        'cipd', 'create', '-pkg-def', pkg_def])
+
+  def testUploadCIPDPackageAllArgs(self):
+    """Verifies UploadCIPDPackage with all args specified."""
+    pkg_def = '/path/to/pkg.yaml'
+    refs = ['test', 'latest']
+    tags = {'tag1': 'this', 'tag2': 'that'}
+    verification_timeout = '5m0s'
+    service_account_json = '/path/to/token.json'
+
+    cipd_upload_utils.UploadCIPDPackage(
+        self._executor,
+        pkg_def,
+        refs=refs,
+        tags=tags,
+        verification_timeout=verification_timeout,
+        service_account_json=service_account_json,
+        dry_run=False)
+
+    self._executor.exec_subprocess.assert_called_once_with([
+        'cipd', 'create',
+        '-pkg-def', pkg_def,
+        '-ref', 'test',
+        '-ref', 'latest',
+        '-tag', 'tag1:this', '-tag', 'tag2:that',
+        '-verification-timeout', verification_timeout,
+        '-service-account-json', service_account_json,])
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/clang_format/clang-format-diff.py b/cq/scripts/helpers/clang_format/clang-format-diff.py
new file mode 100644
index 0000000..ef59d17
--- /dev/null
+++ b/cq/scripts/helpers/clang_format/clang-format-diff.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python
+#
+#===- clang-format-diff.py - ClangFormat Diff Reformatter ----*- python -*--===#
+#
+#                     The LLVM Compiler Infrastructure
+#
+# This file is distributed under the University of Illinois Open Source
+# License. See LICENSE.TXT for details.
+#
+#===------------------------------------------------------------------------===#
+
+r"""
+ClangFormat Diff Reformatter
+============================
+
+This script reads input from a unified diff and reformats all the changed
+lines. This is useful to reformat all the lines touched by a specific patch.
+Example usage for git/svn users:
+
+  git diff -U0 HEAD^ | clang-format-diff.py -p1 -i
+  svn diff --diff-cmd=diff -x-U0 | clang-format-diff.py -i
+
+"""
+
+from __future__ import absolute_import
+from __future__ import print_function
+import argparse
+import difflib
+import re
+import string
+import subprocess
+import StringIO
+import sys
+
+
+def main():
+  parser = argparse.ArgumentParser(description=
+                                   'Reformat changed lines in diff. Without -i '
+                                   'option just output the diff that would be '
+                                   'introduced.')
+  parser.add_argument('-i', action='store_true', default=False,
+                      help='apply edits to files instead of displaying a diff')
+  parser.add_argument('-p', metavar='NUM', default=0,
+                      help='strip the smallest prefix containing P slashes')
+  parser.add_argument('-regex', metavar='PATTERN', default=None,
+                      help='custom pattern selecting file paths to reformat '
+                      '(case sensitive, overrides -iregex)')
+  parser.add_argument('-iregex', metavar='PATTERN', default=
+                      r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc|js|ts|proto'
+                      r'|protodevel|java)',
+                      help='custom pattern selecting file paths to reformat '
+                      '(case insensitive, overridden by -regex)')
+  parser.add_argument('-sort-includes', action='store_true', default=False,
+                      help='let clang-format sort include blocks')
+  parser.add_argument('-v', '--verbose', action='store_true',
+                      help='be more verbose, ineffective without -i')
+  parser.add_argument('-style',
+                      help='formatting style to apply (LLVM, Google, Chromium, '
+                      'Mozilla, WebKit)')
+  parser.add_argument('-binary', default='clang-format',
+                      help='location of binary to use for clang-format')
+  args = parser.parse_args()
+
+  # Extract changed lines for each file.
+  filename = None
+  lines_by_file = {}
+  for line in sys.stdin:
+    match = re.search('^\+\+\+\ (.*?/){%s}(\S*)' % args.p, line)
+    if match:
+      filename = match.group(2)
+    if filename is None:
+      continue
+
+    if args.regex is not None:
+      if not re.match('^%s$' % args.regex, filename):
+        continue
+    else:
+      if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE):
+        continue
+
+    match = re.search('^@@.*\+(\d+)(,(\d+))?', line)
+    if match:
+      start_line = int(match.group(1))
+      line_count = 1
+      if match.group(3):
+        line_count = int(match.group(3))
+      if line_count == 0:
+        continue
+      end_line = start_line + line_count - 1;
+      lines_by_file.setdefault(filename, []).extend(
+          ['-lines', str(start_line) + ':' + str(end_line)])
+
+  # Reformat files containing changes in place.
+  for filename, lines in lines_by_file.items():
+    if args.i and args.verbose:
+      print('Formatting', filename)
+    command = [args.binary, filename]
+    if args.i:
+      command.append('-i')
+    if args.sort_includes:
+      command.append('-sort-includes')
+    command.extend(lines)
+    if args.style:
+      command.extend(['-style', args.style])
+    p = subprocess.Popen(command, stdout=subprocess.PIPE,
+                         stderr=None, stdin=subprocess.PIPE)
+    stdout, _ = p.communicate()
+    if p.returncode != 0:
+      sys.exit(p.returncode);
+
+    if not args.i:
+      with open(filename) as f:
+        code = f.readlines()
+      formatted_code = StringIO.StringIO(stdout).readlines()
+      diff = difflib.unified_diff(code, formatted_code,
+                                  filename, filename,
+                                  '(before formatting)', '(after formatting)')
+      diff_string = string.join(diff, '')
+      if len(diff_string) > 0:
+        sys.stdout.write(diff_string)
+
+if __name__ == '__main__':
+  main()
diff --git a/cq/scripts/helpers/commit_message_utils.py b/cq/scripts/helpers/commit_message_utils.py
new file mode 100644
index 0000000..6c9e934
--- /dev/null
+++ b/cq/scripts/helpers/commit_message_utils.py
@@ -0,0 +1,17 @@
+"""Encapsulates regex matchers to detect magic keywords in commit messages."""
+
+from __future__ import absolute_import
+import re
+
+REVERT = '^Revert'
+REVERT_MSG = '^This reverts commit [0-9a-f]{5,40}'
+CHERRY_PICK_MSG = r'\(cherry picked from commit [0-9a-f]{5,40}\)'
+
+
+def is_revert_commit_message(commit_message):
+  return bool(re.search(REVERT, commit_message, re.MULTILINE) or
+              re.search(REVERT_MSG, commit_message, re.MULTILINE))
+
+
+def is_cherry_pick_commit_message(commit_message):
+  return bool(re.search(CHERRY_PICK_MSG, commit_message, re.MULTILINE))
diff --git a/cq/scripts/helpers/commit_message_utils_test.py b/cq/scripts/helpers/commit_message_utils_test.py
new file mode 100644
index 0000000..8eefea6
--- /dev/null
+++ b/cq/scripts/helpers/commit_message_utils_test.py
@@ -0,0 +1,42 @@
+"""Tests for commit_message_utils."""
+import unittest
+
+from cq.scripts.helpers import commit_message_utils
+
+EXAMPLE_REVERT_COMMIT_MESSAGE = """
+Revert "Split AssistantService's implementation off CastService's"
+
+Revert the commit because it breaks Cast on AT.
+
+This reverts commit 9c2d5e2bb6d51a8ad6cb9b0fa9c5e867cc525d7a.
+
+Test: Cast boots again on Jasper on O
+Bug: 110371959
+
+Change-Id: I3fa23e842985bb673b60ea959b9bff3a484056f3
+"""
+
+EXAMPLE_NON_REVERT_COMMIT_MESSAGE = """
+Split AssistantService's implementation off CastService's
+
+Still uses v2 and then Mojo to talk to libassistant, both of which will
+now be able to be removed.
+
+Bug: 109845832
+Test: For the moment only that it compiles (assistant enabled and not).
+Change-Id: I22552ed2737d75c62947c8394fb0fab2cf17a013
+"""
+
+class CommitMessageUtilsTest(unittest.TestCase):
+
+  def testTheCommitMessageThatPromptedChangeInClangFormatStep(self):
+    self.assertTrue(commit_message_utils.is_revert_commit_message(
+        EXAMPLE_REVERT_COMMIT_MESSAGE))
+
+  def testTheCommitMessageForTheChangeThatItWasReverting(self):
+    self.assertFalse(commit_message_utils.is_revert_commit_message(
+        EXAMPLE_NON_REVERT_COMMIT_MESSAGE))
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/coverage_utils.py b/cq/scripts/helpers/coverage_utils.py
new file mode 100644
index 0000000..cc0aca7
--- /dev/null
+++ b/cq/scripts/helpers/coverage_utils.py
@@ -0,0 +1,31 @@
+"""Contains utils for processing coverage files."""
+import pathlib
+
+_RBE_PREFIX = pathlib.Path('/b/f/w')
+
+
+def rewrite_file_paths_in_lcov(input_path, output_path, workspace_path):
+  """Copy the input lcov file to the output, after fixing the paths in it."""
+  with open(output_path, 'w') as out_file:
+    with open(input_path, 'r') as in_file:
+      for line in in_file:
+        if line.startswith('SF:'):
+          path = pathlib.Path(line.removeprefix('SF:').strip())
+          path = _convert_to_workspace_relative_path(path, workspace_path)
+          out_file.write(f'SF:{path}\n')
+        else:
+          out_file.write(line)
+
+
+def _convert_to_workspace_relative_path(path, workspace_path):
+  if not path.is_absolute():
+    raise ValueError(f'Expected absolute paths in lcov file, got {path}')
+  absolute_workspace_path_or_symlink = _fix_rbe_prefix(path, workspace_path)
+  absolute_workspace_path = absolute_workspace_path_or_symlink.resolve()
+  return absolute_workspace_path.relative_to(workspace_path)
+
+
+def _fix_rbe_prefix(path, workspace_path):
+  if path.is_relative_to(_RBE_PREFIX):
+    return workspace_path / path.relative_to(_RBE_PREFIX)
+  return path
diff --git a/cq/scripts/helpers/coverage_utils_test.py b/cq/scripts/helpers/coverage_utils_test.py
new file mode 100644
index 0000000..88d615a
--- /dev/null
+++ b/cq/scripts/helpers/coverage_utils_test.py
@@ -0,0 +1,111 @@
+import contextlib
+import pathlib
+import tempfile
+import unittest
+
+from cq.scripts.helpers import coverage_utils
+
+
+class CoverageUtilsTest(unittest.TestCase):
+
+  def setUp(self):
+    super().setUp()
+    with contextlib.ExitStack() as stack:
+      self._temp_dir_path = pathlib.Path(
+          stack.enter_context(tempfile.TemporaryDirectory()))
+      self.addCleanup(stack.pop_all().close)
+    self._workspace_path = self._temp_dir_path / 'workspace'
+    self._workspace_path.mkdir()
+
+  def test_rewrite_file_paths_in_lcov_with_rbe_prefix(self):
+    real_file = self._workspace_path / 'foo'
+    real_file.touch()
+    input_file = self._temp_dir_path / 'input.lcov'
+    input_file.write_text('\n'.join(_lcov_record('/b/f/w/foo')))
+    output_file = self._temp_dir_path / 'output.lcov'
+
+    coverage_utils.rewrite_file_paths_in_lcov(
+        input_file, output_file, self._workspace_path)
+
+    expected_contents = '\n'.join(_lcov_record('foo'))
+    self.assertEqual(output_file.read_text(), expected_contents)
+
+  def test_rewrite_file_paths_in_lcov_with_symlink(self):
+    real_file = self._workspace_path / 'real'
+    real_file.touch()
+    symlink = self._workspace_path / 'symlink'
+    symlink.symlink_to(real_file)
+    input_file = self._temp_dir_path / 'input.lcov'
+    input_file.write_text('\n'.join(_lcov_record(symlink)))
+    output_file = self._temp_dir_path / 'output.lcov'
+
+    coverage_utils.rewrite_file_paths_in_lcov(
+        input_file, output_file, self._workspace_path)
+
+    expected_contents = '\n'.join(_lcov_record('real'))
+    self.assertEqual(output_file.read_text(), expected_contents)
+
+  def test_rewrite_file_paths_in_lcov_with_rbe_symlink(self):
+    real_file = self._workspace_path / 'real'
+    real_file.touch()
+    symlink = self._workspace_path / 'symlink'
+    symlink.symlink_to(real_file)
+    input_file = self._temp_dir_path / 'input.lcov'
+    input_file.write_text('\n'.join(_lcov_record('/b/f/w/symlink')))
+    output_file = self._temp_dir_path / 'output.lcov'
+
+    coverage_utils.rewrite_file_paths_in_lcov(
+        input_file, output_file, self._workspace_path)
+
+    expected_contents = '\n'.join(_lcov_record('real'))
+    self.assertEqual(output_file.read_text(), expected_contents)
+
+  def test_rewrite_file_paths_in_lcov_with_absolute_path(self):
+    real_file = self._workspace_path / 'real'
+    real_file.touch()
+    input_file = self._temp_dir_path / 'input.lcov'
+    input_file.write_text('\n'.join(_lcov_record(real_file)))
+    output_file = self._temp_dir_path / 'output.lcov'
+
+    coverage_utils.rewrite_file_paths_in_lcov(
+        input_file, output_file, self._workspace_path)
+
+    expected_contents = '\n'.join(_lcov_record('real'))
+    self.assertEqual(output_file.read_text(), expected_contents)
+
+  def test_rewrite_file_paths_in_lcov_with_absolute_missing_path(self):
+    real_file = self._workspace_path / 'real'
+    input_file = self._temp_dir_path / 'input.lcov'
+    input_file.write_text('\n'.join(_lcov_record(real_file)))
+    output_file = self._temp_dir_path / 'output.lcov'
+
+    coverage_utils.rewrite_file_paths_in_lcov(
+        input_file, output_file, self._workspace_path)
+
+    expected_contents = '\n'.join(_lcov_record('real'))
+    self.assertEqual(output_file.read_text(), expected_contents)
+
+  def test_rewrite_file_paths_in_lcov_with_relative_path(self):
+    real_file = self._workspace_path / 'real'
+    real_file.touch()
+    input_file = self._temp_dir_path / 'input.lcov'
+    input_file.write_text('\n'.join(_lcov_record('real')))
+    output_file = self._temp_dir_path / 'output.lcov'
+
+    with self.assertRaises(ValueError):
+      coverage_utils.rewrite_file_paths_in_lcov(
+          input_file, output_file, self._workspace_path)
+
+
+def _lcov_record(path):
+  return [
+      f'SF:{path}',
+      'LH:10',
+      'LC:22',
+      'end_of_record',
+      '',
+  ]
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/data_manager.py b/cq/scripts/helpers/data_manager.py
new file mode 100644
index 0000000..721d048
--- /dev/null
+++ b/cq/scripts/helpers/data_manager.py
@@ -0,0 +1,143 @@
+"""DataManager to share data between steps."""
+
+from __future__ import absolute_import
+import copy
+import six
+
+from helpers import repo_utils
+
+
+class DataManager(object):
+  """Manages passing data between build steps."""
+
+  def __init__(self, executor, properties):
+    self._data_store = {}
+    self._executor = executor
+    self._project_path_lookup = None
+    self._project_remote_lookup = None
+    self._project_revision_lookup = None
+    self._properties = properties
+
+  def add_data(self, key, data, replace=False):
+    """Adds data to be retrieved later by other DataManager consumers.
+
+    Args:
+      key: string value to associate the stored data with. Must not already
+           have a value stored with that key.
+      data: any value, not cloned.
+      replace: if True, any data stored in |key| will be replaced.
+               if False, a KeyError is raised if |key| already has a value.
+               (defaults to False)
+
+    Raises:
+      KeyError: if the key already has an associated value and replace is False.
+    """
+    assert isinstance(key, six.string_types)
+    if self._data_store.get(key) == data:
+      return
+    if key in self._data_store and not replace:
+      raise KeyError(key)
+    self._data_store[key] = data
+
+  def clear_project_info(self):
+    """Clears the project info to be reset in the next run."""
+    self._project_path_lookup = None
+    self._project_remote_lookup = None
+    self._project_revision_lookup = None
+
+  def contains_project(self, project):
+    """Boolean if the given project is included in the repo client."""
+    if not self._project_path_lookup:
+      self._project_path_lookup = repo_utils.get_project_path_lookup(
+          self._executor, self.manifest_url)
+    return project in self._project_path_lookup
+
+  def ensure_project_info(self):
+    """Initializes all project lookup tables."""
+    if self._project_path_lookup:
+      return
+    manifest_content = repo_utils.get_manifest(self._executor)
+    self._project_path_lookup = repo_utils.get_project_path_lookup(
+        self._executor, self.manifest_url, manifest=manifest_content)
+    self._project_remote_lookup = repo_utils.get_project_remote_lookup(
+        self._executor, self.manifest_url, manifest=manifest_content)
+    self._project_revision_lookup = repo_utils.get_project_revision_lookup(
+        self._executor, self.manifest_url, self.manifest_branch,
+        manifest=manifest_content)
+
+  def get_data(self, key, default=None):
+    """Returns previously stored data associated with a known key.
+
+    Args:
+      key: string key the data is associated with.
+      default: Default return value if the key is not found.
+
+    Returns:
+      The value associated with the provided key.
+    """
+    assert isinstance(key, six.string_types)
+    return self._data_store.get(key, default)
+
+  def get_project_path(self, project):
+    """Returns the path to the project.
+
+    Args:
+      project: The name of the project (usually patch_project) to lookup.
+
+    Returns:
+      The relative path from the base checkout to the project.
+    """
+    self.ensure_project_info()
+    return self._project_path_lookup.get(project)
+
+  def get_project_path_lookup_table(self):
+    """Returns the project path lookup table."""
+    self.ensure_project_info()
+    return self._project_path_lookup
+
+  def get_project_remote(self, project):
+    """Returns the remote for the project.
+
+    Args:
+      project: The name of the project (usually patch_project) to lookup.
+
+    Returns:
+      The remote for the project.
+    """
+    self.ensure_project_info()
+    return self._project_remote_lookup.get(project)
+
+  def get_project_revision(self, project):
+    """Returns the revision for the project.
+
+    Args:
+      project: The name of the project (usually patch_project) to lookup.
+
+    Returns:
+      The revision for the project.
+    """
+    self.ensure_project_info()
+    return self._project_revision_lookup.get(project)
+
+  def get_property(self, name, default=None):
+    """Returns the requested property value, or default if not found."""
+    if not self._properties:
+      return default
+    return self._properties.get(name, default)
+
+  @property
+  def manifest_branch(self):
+    """Returns the manifest branch to use for this step."""
+    return self.get_property('manifest_branch')
+
+  @property
+  def manifest_url(self):
+    """Returns the manifest url to use for this step."""
+    return self.get_property('manifest_url')
+
+
+class DataManagerMock(DataManager):
+  """Mocks data passing between steps for tests."""
+
+  def get_all_data(self):
+    return copy.deepcopy(self._data_store)
diff --git a/cq/scripts/helpers/diff_utils.py b/cq/scripts/helpers/diff_utils.py
new file mode 100644
index 0000000..fccd46c
--- /dev/null
+++ b/cq/scripts/helpers/diff_utils.py
@@ -0,0 +1,134 @@
+"""Compilation of utility functions for working with file diffs."""
+
+from __future__ import absolute_import
+import re
+
+import six
+
+def diff_to_comments(diff):
+  r"""Converts a unified diff to a map of comments to be added to a review.
+
+  Comments are formatted according to:
+  https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-input
+
+  Example output:
+  {
+   'path/to/foo': [{'line': '1', 'message': '+ bar'},
+                   {'line': '10', 'message': '+ baz\n- foo'}]
+   'path/to/bar': [{'line': '8', 'message': '- biz'}]
+  }
+
+  Args:
+    diff: output from difflib.unified_diff()
+
+  Returns:
+   A dictionary of comments to be added to a review.
+   Keys are filenames and values are lists of dictionaries
+   with 'line' and 'message' entries.
+  """
+  comments = {}
+  tofile = ''
+  line_range = ()
+  message_lines = []
+
+  for line in diff:
+    line = line.strip()
+    if line.startswith('+++'):
+      if message_lines:
+        # We transitioned from one file's diffs to the next,
+        # so add last comment from the previous file
+        _add_comment(tofile, line_range, message_lines, comments)
+        message_lines = []
+      tofile = _get_filename(line)
+    elif line.startswith('---'):
+      # fromfile name is not used
+      continue
+    elif line.startswith('@@') and line.endswith('@@'):
+      if message_lines:
+        _add_comment(tofile, line_range, message_lines, comments)
+        message_lines = []
+      line_range = _get_linerange(line)
+    else:
+      message_lines.append(line)
+
+  if message_lines:
+    _add_comment(tofile, line_range, message_lines, comments)
+
+  return comments
+
+
+def _get_filename(line):
+  """Returns the filename from a unified diff line.
+
+  The line should be of the format:
+  --- filename\tdate\tdate
+  +++ filename\tdate\tdate
+
+  Args:
+    line: A line from a unified diff output
+
+  Returns:
+    the filename in the line, if it exists. '' otherwise.
+  """
+  assert isinstance(line, six.string_types)
+  assert line.startswith('---') or line.startswith('+++')
+
+  if len(line) > 4:
+    return line[4:].split('\t', 1)[0]
+  else:
+    return ''
+
+
+def _get_linerange(line):
+  """Returns a tuple of (start_line, end_line) for this change.
+
+  The line should be of the format:
+  @@ -#,# +#,# @@
+
+  Args:
+    line: A line from a unified diff output
+
+  Returns:
+    The line number of the tofile that the change starts at.
+  """
+  assert isinstance(line, six.string_types)
+  assert line.startswith('@@')
+  assert line.endswith('@@')
+
+  matches = re.search(r'-(\d+)(,(\d+))? \+(\d+)(,(\d+))?', line)
+  start_line = matches.group(4)
+  diff_length = matches.group(6) or 1
+  end_line = str(int(start_line) + int(diff_length) - 1)
+  return (start_line, end_line)
+
+
+def _add_comment(filename, line_range, message_lines, comments):
+  """Add a comment to the list of comments.
+
+  Args:
+    filename: the file the comment applies to
+    line_range: the (start_line, end_line) numbers that this comment applies to
+    message_lines: a list of lines to put in the comment
+    comments: the dictionary of comments to add this comment to
+  """
+  assert message_lines
+  assert len(line_range) == 2
+
+  start_line, end_line = line_range
+  start_character = 0
+  end_character = max(len(message_lines[-1]) - 1, 0)
+  # If the message line is a diff'd line, it will have an extra character
+  # at the start that the actual file line does not have.
+  if message_lines[-1].startswith(('-', '+')):
+    end_character -= 1
+  # Prepend a space to each line so gerrit doesn't format the "-" as a list.
+  message_lines = [' ' + line for line in message_lines]
+  comment = {'range': {'start_line': str(start_line),
+                       'start_character': str(start_character),
+                       'end_line': str(end_line),
+                       'end_character': str(end_character)},
+             'message': '\n'.join(message_lines)}
+  if filename in comments:
+    comments[filename].append(comment)
+  else:
+    comments[filename] = [comment]
diff --git a/cq/scripts/helpers/diff_utils_test.py b/cq/scripts/helpers/diff_utils_test.py
new file mode 100644
index 0000000..3e3d9eb
--- /dev/null
+++ b/cq/scripts/helpers/diff_utils_test.py
@@ -0,0 +1,203 @@
+"""Tests for diff_utils."""
+
+from __future__ import absolute_import
+import difflib
+import os
+import sys
+import unittest
+
+# Allow imports from cq/scripts folder
+sys.path.insert(
+    0,
+    os.path.realpath(
+        os.path.join(os.path.dirname(__file__), os.pardir)))
+from helpers import diff_utils
+
+FROM_FILE = 'FROM_FILE'
+TO_FILE = 'TO_FILE'
+
+
+class DiffUtilsTest(unittest.TestCase):
+
+  # pylint: disable=invalid-name
+  def diff_strings(self, from_string, to_string, from_name, to_name):
+    return difflib.unified_diff(from_string.splitlines(True),
+                                to_string.splitlines(True),
+                                fromfile=from_name,
+                                tofile=to_name,
+                                n=0)
+
+  def testEmptyDiff(self):
+    """Tests creating comments from an empty diff."""
+    empty = ''
+
+    diff = self.diff_strings(empty, empty, FROM_FILE, TO_FILE)
+    comments = diff_utils.diff_to_comments(diff)
+
+    self.assertEquals(comments, {})
+
+  def testSingleLineSingleDiff(self):
+    """Tests creating a comment from a diff made form a single line."""
+    from_lines = 'This line is foo.'
+    to_lines = 'This line is bar.'
+
+    diff = self.diff_strings(from_lines, to_lines, FROM_FILE, TO_FILE)
+    comments = diff_utils.diff_to_comments(diff)
+
+    self.assertEqual(len(comments), 1)
+    self.assertIn(TO_FILE, comments)
+    self.assertEqual(len(comments[TO_FILE]), 1)
+
+    expected_range = {'start_line': '1',
+                      'start_character': '0',
+                      'end_line': '1',
+                      'end_character': '16'}
+    comment_range = comments[TO_FILE][0]['range']
+    self.assertEqual(expected_range, comment_range)
+
+    message = comments[TO_FILE][0]['message']
+    self.assertIn('-' + from_lines, message)
+    self.assertIn('+' + to_lines, message)
+    self.assertEqual(len(message.splitlines()), 2)
+
+  def testMultipleLineSingleDiff(self):
+    """Tests creating comments from a diff made of multiple lines."""
+    from_lines = ('This line is the same.\n'
+                  'This line is foo.\n'
+                  'This line is foo also.\n'
+                  'This line is the same.')
+    to_lines = ('This line is the same.\n'
+                'This line is bar.\n'
+                'This line is bar also.\n'
+                'This line is the same.')
+
+    diff = self.diff_strings(from_lines, to_lines, FROM_FILE, TO_FILE)
+    comments = diff_utils.diff_to_comments(diff)
+
+    self.assertEqual(len(comments), 1)
+    self.assertIn(TO_FILE, comments)
+    self.assertEqual(len(comments[TO_FILE]), 1)
+
+    expected_range = {'start_line': '2',
+                      'start_character': '0',
+                      'end_line': '3',
+                      'end_character': '21'}
+    comment_range = comments[TO_FILE][0]['range']
+    self.assertEqual(expected_range, comment_range)
+
+    message = comments[TO_FILE][0]['message']
+    self.assertEqual(message.count('-'), 2)
+    self.assertEqual(message.count('+'), 2)
+    self.assertEqual(len(message.splitlines()), 4)
+
+  def testMultipleLineMultipleDiff(self):
+    """Tests creating multiple comments from a multi-part diff."""
+    from_lines = ('This line is the same.\n'
+                  'This line is foo.\n'
+                  'This line is the same.\n'
+                  'This line is foo.\n'
+                  'This line is foo.')
+    to_lines = ('This line is the same.\n'
+                'This line is bar.\n'
+                'This line is the same.\n'
+                'This line is bar.\n'
+                'This line is bar.')
+
+    diff = self.diff_strings(from_lines, to_lines, FROM_FILE, TO_FILE)
+    comments = diff_utils.diff_to_comments(diff)
+
+    self.assertEqual(len(comments), 1)
+    self.assertIn(TO_FILE, comments)
+    self.assertEqual(len(comments[TO_FILE]), 2)
+
+    # First diff
+    expected_range = {'start_line': '2',
+                      'start_character': '0',
+                      'end_line': '2',
+                      'end_character': '16'}
+    comment_range = comments[TO_FILE][0]['range']
+    self.assertEqual(expected_range, comment_range)
+
+    message = comments[TO_FILE][0]['message']
+    self.assertEqual(message.count('-'), 1)
+    self.assertEqual(message.count('+'), 1)
+    self.assertEqual(len(message.splitlines()), 2)
+
+    # Second diff
+    expected_range = {'start_line': '4',
+                      'start_character': '0',
+                      'end_line': '5',
+                      'end_character': '16'}
+    comment_range = comments[TO_FILE][1]['range']
+    self.assertEqual(expected_range, comment_range)
+
+    message = comments[TO_FILE][1]['message']
+    self.assertEqual(message.count('-'), 2)
+    self.assertEqual(message.count('+'), 2)
+    self.assertEqual(len(message.splitlines()), 4)
+
+  def testNoFileName(self):
+    """Tests creating a comment when tofile is not passed to unified_diff()."""
+    from_lines = 'This line is foo.'
+    to_lines = 'This line is bar.'
+
+    diff = difflib.unified_diff(from_lines, to_lines)
+    comments = diff_utils.diff_to_comments(diff)
+
+    self.assertEqual(len(comments), 1)
+    self.assertIn('', comments)
+    self.assertEqual(len(comments['']), 1)
+
+  def testSpacesForGerritFormatting(self):
+    """Tests that each line of a file level comment starts with a space."""
+    from_lines = ('This line is the same.\n'
+                  'This line is foo.\n'
+                  'This line is foo also.\n'
+                  'This line is the same.')
+    to_lines = ('This line is the same.\n'
+                'This line is bar.\n'
+                'This line is bar also.\n'
+                'This line is the same.')
+
+    diff = self.diff_strings(from_lines, to_lines, FROM_FILE, TO_FILE)
+    comments = diff_utils.diff_to_comments(diff)
+
+    message = comments[TO_FILE][0]['message']
+    for line in message.splitlines():
+      self.assertTrue(line.startswith(' '))
+
+  def testDiffWithMultipleFiles(self):
+    """Tests that a multifile diff puts comments with the proper file."""
+    diff = ['--- foo1.cc\n',
+            '+++ foo1.cc\n',
+            '@@ -123,1 +123,1 @@\n',
+            'message1.0\n',
+            '--- foo2.cc\n',
+            '+++ foo2.cc\n',
+            '@@ -456,1 +456,1 @@\n',
+            'message2.0\n',
+            '@@ -456,1 +456,1 @@\n',
+            'message2.1\n',
+            '--- foo3.cc\n',
+            '+++ foo3.cc\n',
+            '@@ -789,1 +789,1 @@\n',
+            'message3.0\n',]
+    comments = diff_utils.diff_to_comments(diff)
+
+    self.assertEqual(len(comments), 3)
+    self.assertIn('foo1.cc', comments)
+    self.assertIn('foo2.cc', comments)
+    self.assertIn('foo3.cc', comments)
+
+    self.assertEqual(len(comments['foo1.cc']), 1)
+    self.assertEqual(len(comments['foo2.cc']), 2)
+    self.assertEqual(len(comments['foo3.cc']), 1)
+
+    self.assertIn('message1.0', comments['foo1.cc'][0]['message'])
+    self.assertIn('message2.0', comments['foo2.cc'][0]['message'])
+    self.assertIn('message2.1', comments['foo2.cc'][1]['message'])
+    self.assertIn('message3.0', comments['foo3.cc'][0]['message'])
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/env_constants.py b/cq/scripts/helpers/env_constants.py
new file mode 100644
index 0000000..56cc72e
--- /dev/null
+++ b/cq/scripts/helpers/env_constants.py
@@ -0,0 +1,10 @@
+"""Environmental constants utilized by multiple CQ scripts."""
+import pathlib
+
+
+def get_mosaic_role_path():
+  return pathlib.Path('/usr', 'local', 'google', 'home', 'mosaic-role')
+
+
+def get_continuous_tests_path():
+  return pathlib.Path(get_mosaic_role_path(), 'continuous-tests')
diff --git a/cq/scripts/helpers/gclient_utils.py b/cq/scripts/helpers/gclient_utils.py
new file mode 100644
index 0000000..cc99ef3
--- /dev/null
+++ b/cq/scripts/helpers/gclient_utils.py
@@ -0,0 +1,60 @@
+"""Collection of utilities to work with gclient."""
+
+from __future__ import absolute_import
+import imp
+import logging
+import os
+import pathlib
+
+from helpers import git_utils
+
+_DEPOT_TOOLS_SEARCH_PATHS = [
+    # Newer containers don't include depot_tools so search for it in the
+    # manifest. This is the preferred version because it will be pinned on
+    # release branches and track upstream on master.
+    pathlib.Path('/workspace/checkout/chromium/tools/depot_tools'),
+    # Legacy containers have depot_tools installed at this path.
+    pathlib.Path('/workspace/depot_tools'),
+]
+
+
+def find_depot_tools():
+  for path in _DEPOT_TOOLS_SEARCH_PATHS:
+    if path.exists():
+      return path
+
+  raise ValueError('Cannot find a suitable installation of depot_tools')
+
+
+def disable_depot_tools_auto_update_cmd():
+  depot_tools_path = find_depot_tools()
+  return [
+      'python3',
+      str(depot_tools_path / 'update_depot_tools_toggle.py'),
+      '--disable',
+  ]
+
+
+def gclient_sync_cmd(executor, jobs=None, extra_args=None):
+  """Returns the command list to run gclient sync."""
+  depot_tools_path = find_depot_tools()
+  logging.info(
+      'Using depot_tools at %s (%s)',
+      depot_tools_path,
+      git_utils.get_head_sha(executor, str(depot_tools_path)),
+  )
+
+  # See go/authenticating-cipd-on-nest-infra for more context on this command.
+  luci_auth_wrapped_cmd = [
+      str(depot_tools_path / 'luci-auth'), 'context', '-disable-git-auth', '-service-account-json',
+      ':gce', '--',
+      str(depot_tools_path / 'gclient'), 'sync', '--force', '--with_branch_heads',
+      '--delete_unversioned_trees', '--reset'
+  ]
+
+  if jobs is not None:
+    luci_auth_wrapped_cmd.append('--jobs={}'.format(jobs))
+  if extra_args is not None:
+    luci_auth_wrapped_cmd.extend(extra_args)
+
+  return luci_auth_wrapped_cmd
diff --git a/cq/scripts/helpers/gcs_utils.py b/cq/scripts/helpers/gcs_utils.py
new file mode 100644
index 0000000..1537d8a
--- /dev/null
+++ b/cq/scripts/helpers/gcs_utils.py
@@ -0,0 +1,30 @@
+"""Collection of utilities to work with Google Cloud Storage and gsutil."""
+
+import os
+
+
+def gsutil():
+  """Returns the path to the gsutil binary."""
+  # Search the normal install path for gsutil first. This should succeed
+  # most of the time. (A notable exception being GCE instances, which
+  # have gsutil on the PATH somewhere else.)
+  gsutil_path = os.path.expanduser('~/gsutil/gsutil')
+  if os.path.exists(gsutil_path):
+    return gsutil_path
+
+  # Otherwise, assume that gsutil is on the PATH so that this will find it.
+  return 'gsutil'
+
+
+def upload_directory_cmd(local_dir, gcs_dir):
+  """Command to upload the contents of the local directory to the GCS bucket.
+
+  Note that this assumes a gsutil install located at ~/gsutil.
+
+  Args:
+    local_dir: local directory to upload the contents of (e.g. path/to/foo)
+    gcs_dir: directory on GCS to upload to (e.g. gs://my-bucket/foo)
+  Returns:
+    The command (as a list) to execute to upload a directory to GCS.
+  """
+  return [gsutil(), 'cp', '-r', local_dir, gcs_dir]
diff --git a/cq/scripts/helpers/generate_gn_helper.sh b/cq/scripts/helpers/generate_gn_helper.sh
new file mode 100755
index 0000000..adfdfe4
--- /dev/null
+++ b/cq/scripts/helpers/generate_gn_helper.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+#
+# Helper script to run generate_gn from the CQ (and python scripts in general)
+
+function usage {
+  echo "Usage: generate_gn_helper.sh <path-to-chromecast_build_env.sh> <gn-root-dir>"
+  echo "chromecast_build_env.sh can be found at chromium/src/chromecast/internal/build"
+  echo "gn-root-dir is usually chromium/src (but in general is where the .gn file is)"
+}
+
+function main {
+
+  if [[ "$#" -ne 2 ]]; then
+    usage
+    exit 1
+  fi
+
+  local chromecast_build_env_script="$1"
+  local gn_root_dir="$2"
+
+  if [[ ! -e "${chromecast_build_env_script}" ]]; then
+    echo "${chromecast_build_env_script} does not exist."
+    usage
+    exit 1
+  fi
+
+  if [[ ! -e "${gn_root_dir}" ]]; then
+    echo "${gn_root_dir} does not exist."
+    usage
+    exit 1
+  fi
+
+  (
+    # Source chromecast_build_env.sh to get generate_gn and setup_chrome_env_*
+    . "${chromecast_build_env_script}"
+
+    cd "${gn_root_dir}"
+    setup_chrome_env_clang_chromecast `pwd -P` Eng
+    generate_gn --check
+  )
+}
+
+main "$@"
diff --git a/cq/scripts/helpers/git_utils.py b/cq/scripts/helpers/git_utils.py
new file mode 100644
index 0000000..87c420b
--- /dev/null
+++ b/cq/scripts/helpers/git_utils.py
@@ -0,0 +1,211 @@
+"""Collection of utility functions to work with git."""
+
+from __future__ import absolute_import
+import collections
+import os
+import re
+import six
+
+CHERRY_PICK_MSG = r'\(cherry picked from commit [0-9a-f]{5,40}\)'
+SHA_PATTERN = r'[a-fA-F0-9]{40}'
+
+# `git diff` uses /dev/null as a placeholder name for new/deleted files.
+FILENAME_PLACEHOLDER = '/dev/null'
+
+
+def check_history_depth(executor, directory):
+  depth = executor.exec_subprocess(
+      ['git', 'rev-list', '--count', 'HEAD'], cwd=directory, check_output=True)
+  try:
+    return int(depth)
+  except ValueError:
+    return 0
+
+
+def changed_files(executor,
+                  file_extensions=None,
+                  directory=None,
+                  file_exclusion_pattern=None,
+                  depth=1):
+  """Returns a list of names of changed files in newest |depth| commits.
+
+  Args:
+    executor: Executes the subprocess.
+    file_extensions: Filter output to only files with matching extensions. (e.g.
+      ['.cc', '.h'] for C++ files)
+    directory: Directory to execute the subprocess in.
+    file_exclusion_pattern: A regex pattern to filter files with. If the pattern
+      matches a file that file will not be included in the changed file list.
+    depth: The number of commits to find files from.
+
+  Returns:
+    List of names of changed files.
+  """
+  diff_output = executor.exec_subprocess([
+      'git', 'diff', 'HEAD~{}'.format(depth), '--name-only', '--diff-filter=AMR'
+  ],
+                                         cwd=directory,
+                                         check_output=True)
+
+  if not diff_output:
+    return []
+
+  changed_filenames = diff_output.strip().split('\n')
+
+  return filtered_files(
+      changed_filenames,
+      file_extensions=file_extensions,
+      file_exclusion_pattern=file_exclusion_pattern)
+
+
+def get_changed_files_in_all_patches(executor,
+                                     patches,
+                                     path_lookup,
+                                     file_extensions=None,
+                                     file_exclusion_pattern=None):
+  """Returns every file that is changed as a result of applying |patches|.
+
+  This method is useful when determining all files that have changed when a
+  series of patches have been applied via the Depends-On tag. This method
+  supports patches in multiple projects, as well as multiple patches in a
+  single project.
+
+  Args:
+    executor: Executes the subprocess.
+    patches: A list of all of the patches applied to the checkout. For more
+      detail on the entries in this list, see scripts/slave/base_step.py.
+    path_lookup: A dictionary mapping project name to path.
+    file_extensions: Filter output to only files with matching extensions. (e.g.
+      ['.cc', '.h'] for C++ files)
+    file_exclusion_pattern: A regex pattern to filter files with. If the pattern
+      matches a file that file will not be included in the changed file list.
+
+  Returns:
+    A list of file paths relative to the checkout root directory.
+  """
+
+  # |project_depth| is a dictionary mapping each project name to the number of
+  # patches that have been applied on top of that project's HEAD. Here's an
+  # example of what this might look like:
+  # {
+  #   "chromecast/internal": 1,
+  #   "chromium/src": 2,
+  #   "some_other_project": 1
+  # }
+  project_depths = collections.Counter(
+      patch['project'] for patch in patches if patch['project'] in path_lookup)
+
+  # |path_lookup| dictionary uses real paths relative to the root checkout
+  # directory. However, some tools like gn expect paths to be symlinks, e.g.
+  # chromium/src/chromecast/internal/receiver/app/dummy.cc instead of
+  # cast/internal/receiver/app/dummy.cc. Create a dictionary here so that
+  # symlinks can be returned for some projects.
+  file_prefix_dict = {
+      "chromecast/internal": "chromium/src/chromecast/internal"
+  }
+  file_prefix_dict = path_lookup.get_dict() | file_prefix_dict
+
+  # For each project, calculate the list of changed files, concatenate each file
+  # path with the project's path from checkout root, and append them to a list.
+  files = []
+  for project in project_depths.keys():
+    depth = project_depths[project]
+    if depth < check_history_depth(executor, path_lookup[project]):
+      files.extend([
+          os.path.join(file_prefix_dict[project], f)
+          for f in changed_files(
+              executor, file_extensions, path_lookup[project],
+              file_exclusion_pattern, depth)
+      ])
+  return files
+
+
+def filtered_files(filenames,
+                   file_extensions=None,
+                   file_exclusion_pattern=None):
+  """Filters the list of files.
+
+  Args:
+    filenames: A list of filenames to filter.
+    file_extensions: Filter output to only files with matching extensions. (e.g.
+      ['.cc', '.h'] for C++ files)
+    file_exclusion_pattern: A regex pattern to filter files with. If the pattern
+      matches a file that file will not be included in the changed file list.
+
+  Returns:
+    List of files after applying the filters.
+  """
+  if file_extensions:
+    filenames = [f for f in filenames if f.endswith(tuple(file_extensions))]
+  if file_exclusion_pattern:
+    filenames = [
+        f for f in filenames if not re.search(file_exclusion_pattern, f)
+    ]
+  return filenames
+
+
+def commit_message(executor, directory=None):
+  """Returns the most recent git commit message.
+
+  Note that this does not include the Author: or Date: fields,
+  only the message body.
+
+  Args:
+    executor: Executes the subprocess.
+    directory: Directory to execute the subprocess in (defaults to cwd)
+
+  Returns:
+    The most recent git commit message.
+  """
+  return executor.exec_subprocess(['git', 'log', '--format=%B', '-n', '1'],
+                                  cwd=directory,
+                                  check_output=True)
+
+
+def is_cherry_pick(executor, directory=None):
+  """Returns if the most recent commit is a cherry pick.
+
+  Args:
+    executor: Executes the subprocess.
+    directory: Directory to execute the subproccess in (defaults to cwd)
+
+  Returns:
+    True iff the most recent commit is a cherry pick.
+  """
+  return bool(
+      re.search(CHERRY_PICK_MSG, commit_message(executor, directory),
+                re.MULTILINE))
+
+
+def commit_author_email(executor, directory=None):
+  """Returns the author email of the most recent git commit.
+
+  Args:
+    executor: Executes the subprocess.
+    directory: Directory to execute the subprocess in (defaults to cwd)
+
+  Returns:
+    The email of the author of the most recent git commit.
+  """
+  author = executor.exec_subprocess(['git', 'log', '--format=%ae', '-n', '1'],
+                                    cwd=directory,
+                                    check_output=True).strip()
+  # We have to first convert to a unicode str, then decode removing unicode
+  # chars.
+  return six.text_type(author.decode('utf-8')).encode('ascii', errors='ignore')
+
+
+def is_sha(revision):
+  """Returns True if revision looks like a SHA."""
+  return re.match(SHA_PATTERN, revision)
+
+
+def get_head_sha(executor, directory):
+  """Returns the SHA of the HEAD revision for the given directory,
+
+  or None if the directory specified is not a git repository
+  """
+
+  cmd_get_sha = ['git', 'rev-parse', 'HEAD']
+  returncode, stdout, _ = executor.exec_subprocess(cmd_get_sha, cwd=directory)
+  return stdout.strip() if returncode == 0 else None
diff --git a/cq/scripts/helpers/git_utils_test.py b/cq/scripts/helpers/git_utils_test.py
new file mode 100755
index 0000000..4e4a812
--- /dev/null
+++ b/cq/scripts/helpers/git_utils_test.py
@@ -0,0 +1,152 @@
+"""Tests for git_utils."""
+
+from __future__ import absolute_import
+import os
+import sys
+import unittest
+
+# Allow imports from cq/scripts folder
+sys.path.insert(
+    0,
+    os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir)))
+from helpers import git_utils
+from helpers import repo_utils
+
+
+# A mock executor that keeps simple git state.
+class MockExecutor(object):
+
+  def __init__(self):
+    self.current_branch = 'HEAD'
+    self.remote = ''
+    self.remote_branch = ''
+    self._handlers = []
+
+  def add_handler(self, handler):
+    self._handlers.append(handler)
+
+  def exec_subprocess(self, command, cwd=None, check_output=False):
+
+    for handler in self._handlers:
+      result = handler(command, cwd)
+      if result:
+        if check_output:
+          # check_output option only cares about stdout
+          return result[1]
+        return result
+
+    if command[:2] == ['git', 'rev-parse']:
+      return (0, self.current_branch, '')
+    elif command[:2] == ['git', 'checkout']:
+      self.current_branch = command[command.index('-B') + 1]
+      self.remote, self.remote_branch = command[-1].split('/')
+
+    return (0, '', '')
+
+
+class GitUtilsTest(unittest.TestCase):
+
+  def testNoFilterFiles(self):
+    """Tests with no filter."""
+    filenames = [
+        'something.py',
+        'something_else.cc'
+    ]
+    filtered_files = git_utils.filtered_files(filenames)
+    self.assertEquals(filenames, filtered_files)
+
+  def testNoFilteredFiles(self):
+    """Tests filtering files by extension."""
+    filenames = [
+        'something.py',
+        'something_else.cc'
+    ]
+    filtered_files = git_utils.filtered_files(filenames,
+                                              file_extensions=['.py', '.cc'])
+    self.assertEquals(2, len(filtered_files))
+    self.assertIn('something.py', filtered_files)
+    self.assertIn('something_else.cc', filtered_files)
+
+  def testFilteredFiles(self):
+    """Tests filtering files by extension."""
+    filenames = [
+        'something.py',
+        'something_else.cc'
+    ]
+    filtered_files = git_utils.filtered_files(filenames,
+                                              file_extensions=['.py'])
+    self.assertEquals(1, len(filtered_files))
+    self.assertIn('something.py', filtered_files)
+
+  def testFilteredFilesExclusion(self):
+    """Tests filtering files by pattern."""
+    filenames = [
+        'something.py',
+        'something_foo_pb2.py',
+        'something_else.cc'
+    ]
+    filtered_files = git_utils.filtered_files(
+        filenames,
+        file_extensions=['.py'],
+        file_exclusion_pattern=r'^.+_pb2.py$')
+    self.assertEquals(1, len(filtered_files))
+    self.assertIn('something.py', filtered_files)
+
+  def testGetAllFiles(self):
+    """Test getting all files affected by a group of patches."""
+    patches = [
+        {'project': 'chromium/src', 'patchset_number': 12345},
+        {'project': 'chromium/src', 'patchset_number': 67890},
+        {'project': 'chromecast/internal', 'patchset_number': 34567},
+        {'project': 'test', 'patchset_number': 89012},
+        {'project': 'project_with_no_path', 'patchset_number': 1}]
+
+    path_lookup = repo_utils.ProjectLookupTable({
+        'chromium/src': 'chromium/src',
+        'chromecast/internal': 'cast/internal',
+        'test': 'test'})
+
+    def handle_git_revlist(cmd, cwd):
+      if cmd == ['git', 'rev-list', '--count', 'HEAD']:
+        return (0, '200', '')
+      return None
+
+    def handle_git_diff(cmd, cwd):
+      if cmd[:2] != ['git', 'diff'] or '--name-only' not in cmd:
+        return None
+      if cwd == 'chromium/src':
+        self.assertTrue('HEAD~2' in cmd)
+        files = ['base/logging.h', 'media/base/config.cc', 'net/utils.h']
+      elif cwd == 'cast/internal':
+        self.assertTrue('HEAD~1' in cmd)
+        files = ['receiver/client.h', 'base/logging.h']
+      elif cwd == 'test':
+        self.assertTrue('HEAD~1' in cmd)
+        files = ['foo.h']
+      else:
+        return (0, '', '')
+      return (0, '\n'.join(files), '')
+
+    executor = MockExecutor()
+    executor.add_handler(handle_git_revlist)
+    executor.add_handler(handle_git_diff)
+    all_files = git_utils.get_changed_files_in_all_patches(
+        executor, patches, path_lookup)
+    self.assertEqual(set([
+        'chromium/src/base/logging.h',
+        'chromium/src/media/base/config.cc',
+        'chromium/src/net/utils.h',
+        'chromium/src/chromecast/internal/receiver/client.h',
+        'chromium/src/chromecast/internal/base/logging.h',
+        'test/foo.h']), set(all_files))
+
+  def testIsRevisionASha(self):
+    """Tests cases for git_utils.is_sha."""
+    self.assertTrue(
+        git_utils.is_sha('2fe8f3b110d8b117698265e18e432f32837247bb'))
+    self.assertFalse(git_utils.is_sha('master'))
+    self.assertFalse(git_utils.is_sha('unfork_m52'))
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/gn_utils.py b/cq/scripts/helpers/gn_utils.py
new file mode 100644
index 0000000..e02fe56
--- /dev/null
+++ b/cq/scripts/helpers/gn_utils.py
@@ -0,0 +1,216 @@
+"""Compilation of utility functions for working with GN."""
+
+from __future__ import absolute_import
+import collections
+import os
+import re
+
+from helpers import branch_utils
+import six
+
+# TODO(mbjorge|maasen): generate these based on the base path of the repo
+# Default location for GN files
+GN_SHA1 = 'chromium/src/buildtools/linux64/gn.sha1'
+GN_BINARY = 'chromium/src/buildtools/linux64/gn'
+
+# DEPRECATED: Do not reference this variable in new code. Prefer to use a
+# path lookup for the chromium/src project instead.
+GN_CHROMIUM_BASE_DIR = 'chromium/src'
+
+GENERATE_GN_HELPER_SCRIPT = 'scripts/helpers/generate_gn_helper.sh'
+
+GN_EXTENSIONS = ('.gn', '.gni')
+
+GN_DONE_MSG = r'Done. Wrote \d+ targets from \d+ files'
+GN_HEADER_OK_MSG = r'Header dependency check OK'
+GN_ERROR_MSG = (
+    r'ERROR at //(?P<filename>[^:]+):(?P<line>\d+):\d+: (?P<error>.+?)\n')
+GN_ERROR_DIVIDER_MSG = r'___________________'
+GN_FORMAT_ERROR_MSG = r':(?P<linenum>\d+):(?P<column>\d+): '
+
+
+class GnUtilsError(Exception):
+  """Gn Utils specific exception."""
+
+
+def gn_check_error_to_comments(gn_check_output):
+  """Convert a gn check error message to comments for a gerrit review.
+
+  Args:
+    gn_check_output: output from the `gn check` command
+
+  Returns:
+    Comment to be added to a review message.
+    The comment is a dictionary with filenames as keys.
+    Each filename maps to an array of line-numbered comments for that file.
+  """
+  assert isinstance(gn_check_output, six.string_types)
+
+  comments = collections.defaultdict(list)
+
+  # Check that GN check did not succeed
+  if re.search(GN_DONE_MSG, gn_check_output) or re.search(
+      GN_HEADER_OK_MSG, gn_check_output):
+    return comments
+
+  error_msg = re.compile(GN_ERROR_MSG)
+  error_divider_msg = re.compile(GN_ERROR_DIVIDER_MSG)
+
+  filename, linenum, message = '', '', []
+  for line in gn_check_output.splitlines(True):
+    # Look for the start of a new error
+    match = error_msg.match(line)
+    if match:
+      filename = match.group('filename')
+      linenum = match.group('line')
+      message = ['ERROR: {}\n'.format(match.group('error'))]
+      continue
+
+    # Look for the end of the current error
+    match = error_divider_msg.match(line)
+    if match:
+      comment = {'line': linenum, 'message': ''.join(message)}
+      comments[filename].append(comment)
+      filename, linenum, message = '', '', []
+      continue
+
+    # Add the contents of the error to the current comment
+    message.append(line)
+
+  # The last error message does not have a trailing divider
+  if message:
+    comment = {'line': linenum, 'message': ''.join(message)}
+    comments[filename].append(comment)
+
+  return comments
+
+
+def gn_format_error_to_comments(filename, error_msg):
+  """Convert a gn format error message to comments for a gerrit review.
+
+  Args:
+    filename: name of the file that caused the error
+    error_msg: Output message from gn format. Example:
+               "ERROR at /path/to/bad/file.gn:10:12: Invalid bar."
+
+  Returns:
+    Comment to be added to a review message.
+    Example:
+    {'path/to/bad/file.gn': [{'line': 10, 'message': 'Invalid bar.'}]}
+  """
+  assert error_msg.startswith('ERROR at ')
+  assert filename in error_msg
+
+  lines = error_msg.splitlines(True)
+  match = re.search(GN_FORMAT_ERROR_MSG, lines[0])
+  linenum = match.group('linenum')
+  message = error_msg[match.end():]
+
+  return {filename: [{'line': linenum, 'message': message}]}
+
+
+def filepath_to_gn_path(filename):
+  """convert chromium/src/a/b/BUILD.gn to //a/b:*."""
+  if not filename.endswith('BUILD.gn'):
+    raise ValueError('Invalid path %s. Must be a BUILD.gn file.', filename)
+  if not filename.startswith(GN_CHROMIUM_BASE_DIR):
+    raise ValueError('Invalid path %s. Must start with %s',
+                     filename, GN_CHROMIUM_BASE_DIR)
+  target_path = os.path.relpath(os.path.dirname(filename), GN_CHROMIUM_BASE_DIR)
+  if target_path == '.':
+    target_path = ''
+  return  '//{}:*'.format(target_path)
+
+
+def gn_label_to_target_name(gn_label):
+  """Convert '//foo/bar:baz' to 'baz'."""
+  if not gn_label:
+    raise ValueError('Input label must not be empty.')
+  if gn_label[-1] in [':', '/']:
+    raise ValueError('A GN label must not end with {!r}: {}'.format(
+        gn_label[-1], gn_label))
+
+  if ':' in gn_label:
+    return gn_label.split(':')[-1]
+  if '/' in gn_label:
+    return gn_label.split('/')[-1]
+
+  return gn_label
+
+
+def get_gn_binary(executor):
+  """Returns the path to a gn binary.
+
+  This attempts to download the latest version of GN. If the download fails,
+  then an older version will be used. If GN cannot be downloaded and no
+  older version can be found, this throws a GnUtilsError.
+
+  Args:
+    executor: Executes the download of GN via executor.exec_subproces
+
+  Returns:
+    A relative path to the GN binary.
+
+  Raises:
+    GnUtilsError: if GN cannot be found or downloaded.
+  """
+  ret, _, _ = _download_gn(GN_SHA1, executor)
+
+  # If the download succeeded, trust that the binary is in the correct place
+  # and return. If the download fails, an old copy could still exist and be
+  # used.
+  if ret == 0 or _verify_gn_exists():
+    return GN_BINARY
+  raise GnUtilsError('GN binary could not be found or downloaded.')
+
+
+def _verify_gn_exists():
+  """Returns boolean if gn exists."""
+  return os.path.exists(GN_BINARY)
+
+
+def _download_gn(sha1_path, executor):
+  """Downloads the latest GN from google storage.
+
+  Args:
+    sha1_path: Path to gn.sha1 used to download GN.
+               Using gn_utils.GN_SHA1 will result in GN
+               being placed at gn_utils.GN_BINARY
+    executor: Executes the download via executor.exec_subprocess
+
+  Returns:
+    (return_code, stdout, stderr) from the download process
+
+  """
+  assert isinstance(sha1_path, str)
+  assert hasattr(executor, 'exec_subprocess')
+  command = ['download_from_google_storage',
+             '--no_resume',
+             '--platform=linux*',
+             '--no_auth',
+             '--bucket', 'chromium-gn',
+             '-s', sha1_path]
+  return executor.exec_subprocess(command)
+
+
+def  is_gn_check_enabled(manifest_branch, product=None):
+  """Returns true if GN check can be used on |manifest_branch|.
+
+  Since `gn check` will not run cleanly only older branches (<m51, <=1.21),
+  do not enable it.
+
+  Args:
+    manifest_branch: The name of the manifest branch.
+    product: Optional product name (e.g. 'android', 'audio', 'chromecast')
+        Defaults to None.
+  Returns:
+    True if GN check can be used, False otherwise.
+  Raises:
+    ValueError: if the branch name is not recognized.
+  """
+  # Development branch 'master' uses gn check.
+  if manifest_branch == 'master':
+    return True
+
+  return branch_utils.is_branch_equal_to_or_later_than(
+      manifest_branch, '1.23' if product in ['android', 'atv'] else '1.22')
diff --git a/cq/scripts/helpers/gn_utils_test.py b/cq/scripts/helpers/gn_utils_test.py
new file mode 100644
index 0000000..ec7a81b
--- /dev/null
+++ b/cq/scripts/helpers/gn_utils_test.py
@@ -0,0 +1,68 @@
+"""Tests for gn_utils.
+
+Before running these tests, run test_data/gn_utils/generate_test_data.sh with
+a recent version of gn to make sure the test data is up to date.
+"""
+
+from __future__ import absolute_import
+import datetime
+import os
+import re
+import sys
+import unittest
+
+# Allow imports from cq/scripts folder
+sys.path.insert(
+    0,
+    os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir)))
+from helpers import gn_utils
+
+class GnUtilsTest(unittest.TestCase):
+  def testValidGnLabelToTargetName(self):
+    """Tests that gn_label_to_target_name handles valid input properly."""
+    self.assertEqual('baz', gn_utils.gn_label_to_target_name('//foo/bar:baz'))
+    self.assertEqual('baz', gn_utils.gn_label_to_target_name('//foo/bar/baz'))
+    self.assertEqual('foo', gn_utils.gn_label_to_target_name('//foo'))
+    self.assertEqual('qux', gn_utils.gn_label_to_target_name('qux'))
+
+  def testBadGnLabelToTargetName(self):
+    """Tests that gn_label_to_target_name raises exceptions on bad input."""
+    with self.assertRaises(ValueError):
+      gn_utils.gn_label_to_target_name('')
+    with self.assertRaises(ValueError):
+      gn_utils.gn_label_to_target_name(None)
+    with self.assertRaises(ValueError):
+      gn_utils.gn_label_to_target_name('//foo/')
+    with self.assertRaises(ValueError):
+      gn_utils.gn_label_to_target_name('//foo:')
+
+  def testFilePathToGnLabel(self):
+    self.assertEqual(
+        '//:*',
+        gn_utils.filepath_to_gn_path('chromium/src/BUILD.gn'))
+    self.assertEqual(
+        '//foo:*',
+        gn_utils.filepath_to_gn_path('chromium/src/foo/BUILD.gn'))
+    self.assertEqual(
+        '//foo/bar/baz:*',
+        gn_utils.filepath_to_gn_path('chromium/src/foo/bar/baz/BUILD.gn'))
+
+  def testFilePathToGnLabel_RaisesNotBuildGn(self):
+    with self.assertRaises(ValueError):
+      gn_utils.filepath_to_gn_path('chromium/src/x.cc')
+    with self.assertRaises(ValueError):
+      gn_utils.filepath_to_gn_path('chromium/src/foo.gni')
+    with self.assertRaises(ValueError):
+      gn_utils.filepath_to_gn_path('chromium/src/foo/bar')
+
+  def testFilePathToGnLabel_RaisesNotChromiumSrc(self):
+    with self.assertRaises(ValueError):
+      gn_utils.filepath_to_gn_path('src/BUILD.gn')
+    with self.assertRaises(ValueError):
+      gn_utils.filepath_to_gn_path('BUILD.gn')
+    with self.assertRaises(ValueError):
+      gn_utils.filepath_to_gn_path('chromecast/internal/BUILD.gn')
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/luci_recipe_utils.py b/cq/scripts/helpers/luci_recipe_utils.py
new file mode 100644
index 0000000..1cbd80c
--- /dev/null
+++ b/cq/scripts/helpers/luci_recipe_utils.py
@@ -0,0 +1,41 @@
+"""Utility functions for running the LUCI Recipe script."""
+from __future__ import absolute_import
+import os
+
+
+class LUCIRecipeRunner(object):
+
+  def __init__(self, executor, script_path, package_path=None):
+    self._executor = executor
+    self._script_path = script_path
+    self._base_command = [self._script_path]
+    if package_path:
+      self._base_command.extend(['--package', package_path])
+
+  def run_tests(self, verbose=True):
+    """Run recipe expectation tests."""
+    cmd = ['test', 'run']
+    return self.run_command(cmd, verbose=verbose)
+
+  def bundle(self, destination, verbose=True):
+    """Generate a recipe bundle at the specified destination."""
+    cmd = ['bundle', '--destination', destination]
+    return self.run_command(cmd, verbose=verbose)
+
+  def run_command(self, cmd, verbose=True):
+    """Execute a LUCI recipes command."""
+    full_command = self._base_command[:]
+
+    if verbose:
+      full_command.append('--verbose')
+    full_command.extend(cmd)
+
+    try:
+      return self._executor.exec_subprocess(full_command)
+    finally:
+      self.cleanup()
+
+  def cleanup(self):
+    """Delete generated dependencies left from recipe run."""
+    deps_path = os.path.join(os.path.dirname(self._script_path), '.recipe_deps')
+    self._executor.exec_subprocess(['rm', '-rf', deps_path])
diff --git a/cq/scripts/helpers/machine_resource_utils.py b/cq/scripts/helpers/machine_resource_utils.py
new file mode 100644
index 0000000..c65da5a
--- /dev/null
+++ b/cq/scripts/helpers/machine_resource_utils.py
@@ -0,0 +1,53 @@
+"""Utility functions for querying CPU, memory, disk, etc."""
+
+from __future__ import absolute_import
+import os
+
+
+NET_DIRECTION_RX = 0
+NET_DIRECTION_TX = 1
+
+_NET_STAT_ROOT = '/sys/class/net/'
+_NET_STAT_BYTES = {
+    NET_DIRECTION_RX: 'rx_bytes',
+    NET_DIRECTION_TX: 'tx_bytes'
+}
+
+
+def get_free_disk_fraction(directory=None):
+  """Returns free disk space. From 0.0 (disk is full) to 1.0 (disk is empty)."""
+  if not directory:
+    directory = os.getcwd()
+  statvfs = os.statvfs(directory)
+  return float(statvfs.f_bavail) / float(statvfs.f_blocks)
+
+
+def get_network_if():
+  """Returns list of network interfaces on host."""
+  try:
+    return [net_if for net_if in os.listdir(_NET_STAT_ROOT) if net_if != 'lo']
+  except OSError:
+    return []
+
+
+def get_current_directional_traffic(if_list=None, direction=NET_DIRECTION_RX):
+  """Returns current network interfaces directional statistic in bytes."""
+  if not if_list:
+    if_list = get_network_if()
+  bytes_count = 0
+  for net_if in if_list:
+    stat_path = os.path.join(
+        _NET_STAT_ROOT, net_if, 'statistics', _NET_STAT_BYTES[direction])
+    try:
+      with open(stat_path) as stat:
+        bytes_count += int(stat.read())
+    except OSError:
+      pass
+  return bytes_count
+
+
+def get_current_total_traffic(if_list=None):
+  """Returns current rx, tx and total network interfaces statistic in bytes."""
+  rx_bytes = get_current_directional_traffic(if_list, NET_DIRECTION_RX)
+  tx_bytes = get_current_directional_traffic(if_list, NET_DIRECTION_TX)
+  return rx_bytes, tx_bytes, rx_bytes + tx_bytes
diff --git a/cq/scripts/helpers/machine_resource_utils_test.py b/cq/scripts/helpers/machine_resource_utils_test.py
new file mode 100644
index 0000000..3f418da
--- /dev/null
+++ b/cq/scripts/helpers/machine_resource_utils_test.py
@@ -0,0 +1,43 @@
+"""Tests for machine_resource_utils."""
+
+from __future__ import absolute_import
+import os
+import sys
+import unittest
+
+# Allow imports from cq/scripts folder
+sys.path.insert(
+    0,
+    os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir)))
+from helpers import machine_resource_utils
+
+
+class MachineResourceUtilsTest(unittest.TestCase):
+
+  def testGetFreeDiskFraction(self):
+    """Tests getting the free disk fraction."""
+    self.assertLessEqual(0.0, machine_resource_utils.get_free_disk_fraction())
+    self.assertLessEqual(machine_resource_utils.get_free_disk_fraction(), 1.0)
+
+  def testGetNetworkIf(self):
+    """Tests getting network interface list."""
+    if_list = machine_resource_utils.get_network_if()
+    self.assertTrue(if_list)
+    self.assertGreaterEqual(len(if_list), 1)
+
+  def testGetNetworkStat(self):
+    if_list = machine_resource_utils.get_network_if()
+    self.assertGreater(
+        machine_resource_utils.get_current_directional_traffic(
+            if_list=if_list,
+            direction=machine_resource_utils.NET_DIRECTION_RX),
+        0)
+    self.assertGreater(
+        machine_resource_utils.get_current_directional_traffic(
+            if_list=if_list,
+            direction=machine_resource_utils.NET_DIRECTION_TX),
+        0)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/make_utils.py b/cq/scripts/helpers/make_utils.py
new file mode 100644
index 0000000..4f04059
--- /dev/null
+++ b/cq/scripts/helpers/make_utils.py
@@ -0,0 +1,60 @@
+"""Compilation of utility functions for working with Android.mk and make."""
+
+from __future__ import absolute_import
+import collections
+import os
+import re
+
+# Example: path/to/file.c:123:4: error: this is an error
+_MAKE_ERROR_PATTERN = (r'^(?P<filename>[^:]+):(?P<linenum>\d+):(?P<column>\d+):'
+                       r' error: (?P<err_msg>.*)$')
+_MAKE_TOO_MANY_ERRORS_MSG = 'Too many errors! See the logs for the rest.'
+
+
+def make_error_to_comments(make_output, directory):
+  """Convert a make failure message to comments for a gerrit review.
+
+  In general, a when make has an error it prints to stderr and it looks like:
+
+  [...snip...]
+  external/iw/wowlan.c:27:3: error: expected ')' before 'return'
+    return 1;
+    ^
+  external/iw/wowlan.c:182:1: error: expected expression before '}' token
+   }
+   ^
+  cc1.elf: some warnings being treated as errors
+  make: *** [out/obj/EXECUTABLES/iw_intermediates/wowlan.o] Error 1
+  [...snip...]
+
+  And this extracts out the 'error' lines from the rest of the log.
+
+  Args:
+    make_output: stderr output from a make build command. (Note: it's possible
+        this may contain output from other commands. In general though, make
+        errors occur early in the build process and stop the build.)
+    directory: The path to strip off of the beginning of filenames in the
+        make output. Example:
+          make errors are for "path/to/your/files.cc"
+          directory == "path/to" => comments are for "your/files.cc"
+
+  Returns:
+    A dictionary of file comments to be added to a review.
+    Keys are filenames and values are lists of dictionaries with 'line' and
+    'message' entries.
+    { 'path/to/file.cc': [{'line': 5, 'message': 'error!'}, ...]}
+    An empty dict is returned if no comments are found.
+  """
+  comments = collections.defaultdict(list)
+
+  for line in make_output.splitlines():
+    match = re.match(_MAKE_ERROR_PATTERN, line)
+    if match:
+      filename = match.group('filename')
+      # Strip off directory name and slash
+      if directory and filename.startswith(directory):
+        filename = filename[len(directory) + len(os.path.sep):]
+      linenum = int(match.group('linenum'))
+      comments[filename].append({'line': linenum, 'message': line})
+
+  return comments
diff --git a/cq/scripts/helpers/make_utils_test.py b/cq/scripts/helpers/make_utils_test.py
new file mode 100644
index 0000000..d0a0ed7
--- /dev/null
+++ b/cq/scripts/helpers/make_utils_test.py
@@ -0,0 +1,79 @@
+"""Tests for make_utils."""
+
+from __future__ import absolute_import
+import os
+import sys
+import unittest
+
+# Allow imports from cq/scripts folder
+sys.path.insert(
+    0, os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir)))
+from helpers import make_utils
+
+
+class MakeUtilsTest(unittest.TestCase):
+
+  def testSingleError(self):
+    """Test the comments generated for a signle error."""
+    make_output = ('path/to/warn.c:000:0: warning: This is just a warning.\n'
+                   'path/to/file.c:123:4: error: This is an error\n'
+                   '  extra information for the erorr.\n'
+                   'some more lines that come later.\n')
+    directory = 'path/to'
+
+    expected_comments = {
+        'file.c': [{
+            'line': 123,
+            'message': 'path/to/file.c:123:4: error: This is an error'
+        }]
+    }
+
+    self.assertDictEqual(
+        expected_comments,
+        make_utils.make_error_to_comments(make_output, directory))
+
+  def testMultipleErrors(self):
+    """Test the comments generated from multiple different errors."""
+    make_output = ('path/to/warn.c:000:0: warning: This is just a warning.\n'
+                   'path/to/file1.c:123:4: error: This is error 1\n'
+                   '  extra information for the erorr.\n'
+                   'path/to/file1.c:456:7: error: This is error 2\n'
+                   'path/to/other/file3.c:1:1: error: This is error 3\n'
+                   'some more lines that come later.\n')
+    directory = 'path/to'
+
+    expected_comments = {
+        'file1.c': [{
+            'line': 123,
+            'message': 'path/to/file1.c:123:4: error: This is error 1'
+        }, {
+            'line': 456,
+            'message': 'path/to/file1.c:456:7: error: This is error 2'
+        }],
+        'other/file3.c': [{
+            'line': 1,
+            'message': 'path/to/other/file3.c:1:1: error: This is error 3'
+        }]
+    }
+
+    self.assertDictEqual(
+        expected_comments,
+        make_utils.make_error_to_comments(make_output, directory))
+
+  def testNoErrors(self):
+    """Test that no comments are generated if there are no errors."""
+    make_output = ('path/to/warn.c:000:0: warning: This is just a warning.\n'
+                   'path/to/file.c:123:4: warning: This is another warning.\n'
+                   '  extra information for the warning.\n'
+                   'Some more lines with something that is not an error\n')
+    directory = 'path/to'
+
+    expected_comments = {}
+
+    self.assertDictEqual(
+        expected_comments,
+        make_utils.make_error_to_comments(make_output, directory))
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/ninja_utils.py b/cq/scripts/helpers/ninja_utils.py
new file mode 100644
index 0000000..d1c4d92
--- /dev/null
+++ b/cq/scripts/helpers/ninja_utils.py
@@ -0,0 +1,97 @@
+"""Compilation of utility functions for working with ninja."""
+
+from __future__ import absolute_import
+import re
+
+# Example: [123/9876]
+NINJA_PROGRESS_PREFIX = r'^\[\d+/\d+\]'
+NINJA_FAILED_PREFIX = '^FAILED: '
+NINJA_BUILD_STOPPED = '^ninja: build stopped: '
+# Example: 1 error generated.
+# Example: 15 errors generated.
+NINJA_ERRORS_GENERATED = r'^\d+ error(s?) generated\.$'
+NINJA_FILE_INCLUDED_FROM = '^In file included from '
+NINJA_CLANG = r'third_party/llvm-build/Release\+Asserts/bin/clang\+\+'
+# Example: prebuilt/toolchain/mips32r1/bin/mipsel-buildroot-linux-uclibc-g++
+# Example: prebuilt/toolchain/armv7a/bin/armv7a-cros-linux-gnueabi-g++
+NINJA_GXX = r'prebuilt/toolchain/(\S+)(g\+\+)'
+
+NINJA_TOO_MANY_ERRORS_MSG = 'Too many errors! See the logs for the rest.'
+
+
+def ninja_failure_to_comments(ninja_output, max_errors=None):
+  """Convert a ninja failure message to comments for a gerrit review.
+
+  In general, a log is from ninja is expected to look somewhat like:
+
+  [...snip...]
+  [123/9876] CXX obj/path/to/foo.o
+  [124/9876] CXX obj/path/to/bar.o
+  FAILED: obj/path/to/bar.o
+  path/to/compiler/clang++ -FLAG -FLAG ... ... ...
+  path/to/foo:503:27: error: error was here
+      More specific error stuff
+                          ^~~~~
+  1 error generated.
+  [125/9876] STAMP obj/path/to/baz.o
+  [126/9876] ACTION path/to/script
+  ninja: build stopped: subcommand failed
+  [...snip...]
+
+  And this extracts out the FAILED block from the rest of the log.
+
+  Args:
+    ninja_output: output from a ninja build command.
+                  (Note: this can also include output from other tools in
+                   addition to the ninja output, in case ninja is a subcommand
+                   of another tool, such as make.)
+    max_errors: The maximum number of errors to include. If not None,
+                then the returned list will contain at most N error messages.
+                If more than N errors were generated, then the list will also
+                contain an N+1'th message indicating errors were left out.
+
+  Returns:
+    A list of multi-line strings. Each string represents the text of a
+    single "FAILED" message from ninja.
+    An empty list is returned if no comments are found.
+  """
+  comments = set()
+
+  comment, in_failed_block = '', False
+  for line in ninja_output.splitlines(True):
+
+    # Found the start of a failure block
+    if re.search(NINJA_FAILED_PREFIX, line):
+      in_failed_block = True
+      continue
+
+    # Haven't found the start of a failure block yet,
+    # so this line is part of a success. Skip it.
+    if not in_failed_block:
+      continue
+
+    # Found the end of a failure block.
+    if (re.search(NINJA_BUILD_STOPPED, line) or
+        re.search(NINJA_ERRORS_GENERATED, line) or
+        re.search(NINJA_PROGRESS_PREFIX, line)):
+      comments.add(comment.strip('\n'))
+      comment, in_failed_block = '', False
+      continue
+
+    # Ignore these lines, as they are long and do not add enough
+    # useful information compared to other lines
+    if (re.search(NINJA_FILE_INCLUDED_FROM, line) or
+        re.search(NINJA_CLANG, line) or
+        re.search(NINJA_GXX, line)):
+      continue
+
+    # Found a line within a failure block.
+    comment += line
+
+  comments = list(comments)  # pylint: disable=redefined-variable-type
+  if max_errors and isinstance(max_errors, int):
+    max_errors = int(max_errors)
+    if len(comments) > max_errors:
+      comments = comments[0:max_errors] + [NINJA_TOO_MANY_ERRORS_MSG]
+
+  return comments
diff --git a/cq/scripts/helpers/ninja_utils_test.py b/cq/scripts/helpers/ninja_utils_test.py
new file mode 100644
index 0000000..517ddd8
--- /dev/null
+++ b/cq/scripts/helpers/ninja_utils_test.py
@@ -0,0 +1,85 @@
+"""Tests for ninja_utils."""
+
+from __future__ import absolute_import
+import os
+import sys
+import unittest
+
+# Allow imports from cq/scripts folder
+sys.path.insert(0, os.path.realpath(os.path.join(
+    os.path.dirname(__file__), os.pardir)))
+from helpers import ninja_utils
+
+
+class NinjaUtilsTest(unittest.TestCase):
+
+  def testSingleError(self):
+    """Test the comments generated for a single error."""
+    ninja_output = (
+        '[1/230] CXX obj/test/test.o\n'
+        'FAILED: obj test/test.o\n'
+        '../prebuilt/toolchain/mips32r1/bin/mipsel-buildroot-linux-uclibc-g++'
+        ' -MMD -MF obj/test/test1.o -DV8 -fno\n'
+        'In file included from ../../path/to/file:58:0,\n'
+        'This line is expected.\n'
+        'This line is also expected.\n'
+        '[2/230] CXX obj/test/test.o\n')
+
+    expected_comments = ['This line is expected.\nThis line is also expected.']
+    self.assertEqual(expected_comments,
+                     ninja_utils.ninja_failure_to_comments(ninja_output))
+
+  def testMultipleErrors(self):
+    """Test the comments generated from multiple different errors."""
+    ninja_output = (
+        '[1/230] CXX obj/test/test.o\n'
+        'FAILED: obj test/test1.o\n'
+        '../prebuilt/toolchain/mips32r1/bin/mipsel-buildroot-linux-uclibc-g++'
+        ' -MMD -MF obj/test/test1.o -DV8 -fno\n'
+        'In file included from ../../path/to/file:58:0,\n'
+        'This line is expected.\n'
+        'This line is also expected.\n'
+        '[2/230] CXX obj/test/test.o\n'
+        'FAILED: obj test/test2.o\n'
+        '../prebuilt/toolchain/mips32r1/bin/mipsel-buildroot-linux-uclibc-g++'
+        ' -MMD -MF obj/test/test1.o -DV8 -fno\n'
+        'In file included from ../../path/to/file:58:0,\n'
+        'This line is expected again.\n'
+        'This line is also expected again.\n'
+        '[3/230] CXX obj/test/test3.o\n')
+    expected_comments = [
+        'This line is expected.\nThis line is also expected.',
+        'This line is expected again.\nThis line is also expected again.'
+    ]
+    self.assertEqual(
+        set(expected_comments),
+        set(ninja_utils.ninja_failure_to_comments(ninja_output)))
+
+  def testNoErrors(self):
+    """Test that no comments are generated if there are no errors."""
+    ninja_output = (
+        '[1/230] CXX obj/test/test.o\n'
+        '[2/230] CXX obj/test/test.o\n'
+        'Unknown warming that is not handled.\n'
+        'file: error something failed\n')
+    expected_comments = []
+    self.assertEqual(expected_comments,
+                     ninja_utils.ninja_failure_to_comments(ninja_output))
+
+  def testMaxErrors(self):
+    """Test that max_errors limits the errors correctly."""
+    ninja_output = (
+        'FAILED: obj test/test1.o\n'
+        'This line might be in the comments.\n'
+        '[2/230] CXX obj/test/test.o\n'
+        'FAILED: obj test/test2.o\n'
+        'This line might be in the comments. (But not both lines!).\n'
+        '[3/230] CXX obj/test/test3.o\n')
+    # Note that the particular comments that are saved are random.
+    actual_comments = ninja_utils.ninja_failure_to_comments(ninja_output,
+                                                            max_errors=1)
+    self.assertEqual(2, len(actual_comments))
+    self.assertEqual(ninja_utils.NINJA_TOO_MANY_ERRORS_MSG, actual_comments[-1])
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/os_utils.py b/cq/scripts/helpers/os_utils.py
new file mode 100644
index 0000000..8f1def5
--- /dev/null
+++ b/cq/scripts/helpers/os_utils.py
@@ -0,0 +1,24 @@
+"""Utilities for dealing with the operating system."""
+from __future__ import absolute_import
+import contextlib
+import logging
+import os
+
+@contextlib.contextmanager
+def change_cwd(path):
+  old_path = os.path.abspath('.')
+  os.chdir(path)
+  logging.debug(
+      "Changed directory from %s to %s", old_path, os.path.abspath('.'))
+  yield
+  os.chdir(old_path)
+  logging.debug("Changed directory back to %s", old_path)
+
+
+def find_files(root, filename):
+  matching_paths = []
+  for base, _, files in os.walk(root):
+    if filename in files:
+      matching_paths.append(os.path.join(base, filename))
+  return matching_paths
+
diff --git a/cq/scripts/helpers/puppet_utils.py b/cq/scripts/helpers/puppet_utils.py
new file mode 100644
index 0000000..837445d
--- /dev/null
+++ b/cq/scripts/helpers/puppet_utils.py
@@ -0,0 +1,165 @@
+"""Utilities for working with puppet tools."""
+
+from __future__ import absolute_import
+import collections
+import re
+
+_ANSI_COLOR_ESCAPE_SEQUENCE = r'\x1b[^m]*m'
+
+
+def parser_errors_to_comments(errors, directory):
+  """Convert errors reported by the puppet parser in Gerrit comments.
+
+  An example of an error from the puppet parser validator looks like:
+    Error: Could not parse for environment prod:
+      Syntax error at ','; expected '}' at /path/to/puppet/file.pp:27
+
+  Args:
+    errors: List of stderr strings output from the pupper parser.
+    directory: The directory to strip off of any filenames so file comments
+        are rooted at the project directory that Gerrit expects.
+
+  Returns:
+    Dict of Gerrit comments:
+      { 'filename': [{'line': '27', 'message': 'Error!'}, ...], ... }
+  """
+
+  comments = collections.defaultdict(list)
+  for error in errors:
+    error = re.sub(_ANSI_COLOR_ESCAPE_SEQUENCE, '', error)
+    error_parts = error.split()
+    error_location = error_parts[-1]  # /path/to/puppet/file.pp:27
+    abs_filename, line_number = error_location.split(':')
+    rel_filename_start = abs_filename.index(directory) + len(directory)
+    rel_filename = abs_filename[rel_filename_start:]
+    if rel_filename.startswith('/'):
+      rel_filename = rel_filename[1:]
+
+    error_parts[-1] = '{}:{}'.format(rel_filename, line_number)
+
+    comment = {'line': line_number, 'message': ' '.join(error_parts)}
+    comments[rel_filename].append(comment)
+
+  return comments
+
+
+def linter_errors_to_comments(errors, directory):
+  """Convert errors reported by puppet-lint in Gerrit comments.
+
+  Example json error:
+    {
+     "message": "indentation of => is not properly aligned",
+     "line": 6,
+     "column": 12,
+     "token": "#<PuppetLint::Lexer::Token:0x00000001d885a0>",
+     "indent_depth": 13,
+     "newline": false,
+     "newline_indent": "    ",
+     "kind": "warning",
+     "check": "arrow_alignment",
+     "fullpath": "/abs/path/to/puppet/manifests/common.pp",
+     "path": "manifests/common.pp",
+     "filename": "common.pp",
+     "KIND": "WARNING"
+     }
+
+
+  Args:
+    errors: List of error dicts returned from puppet-lint with --json
+    directory: The directory to strip off of any filenames so file comments
+        are rooted at the project directory that Gerrit expects.
+
+  Returns:
+    Dict of Gerrit comments:
+      { 'filename': [{'line': '27', 'message': 'Error!'}, ...], ... }
+  """
+  if not errors:
+    return {}
+
+  # puppet-lint output can wrap errors in nested lists, e.g.
+  # [[error1, error2, ...]], so handle that gracefully.
+  if isinstance(errors[0], list):
+    errors = errors[0]
+
+  comments = collections.defaultdict(list)
+  for error in errors:
+    comment = {'line': str(error['line']), 'message': error['message']}
+    rel_filename_start = error['path'].index(directory) + len(directory)
+    rel_filename = error['path'][rel_filename_start:]
+    if rel_filename.startswith('/'):
+      rel_filename = rel_filename[1:]
+    comments[rel_filename].append(comment)
+  return comments
+
+
+def get_cron_entries(puppet_lines):
+  """Pull out cron entries from a puppet file.
+
+  A cron entry is converted into a dict, where the keys are the attributes
+  and the values are the attribute values (as strings).
+
+  In addition, the dict as a special '_line_number' attribute that records
+  the line number the cron entry started at.
+
+  Example
+    # Puppet entry                      # As dict
+    27: cron { 'my_cron':                   cron_entry = {
+    28:  ensure => present,                    'ensure': 'present',
+    29:  hour   => 2,              ---->       'hour': '2',
+    30:  minute => 0,                          'minute': '0',
+    31:  command => $some_script,              'command': '$some_script',
+                                               '_line_number': '27',
+    32: }                                   }
+
+  Args:
+   puppet_lines: List of lines from a puppet_file (e.g. from readlines()).
+
+  Returns:
+   List of dict objects representing the cron entries.
+  """
+  cron_entries = []
+  within_cron_entry = False
+  curly_brace_count = 0
+  for line_number, line in enumerate(puppet_lines, start=1):
+    if 'cron {' in line:
+      within_cron_entry = True
+      curly_brace_count = 1
+      cron_entry = {'_line_number': str(line_number)}
+      continue
+
+    if not within_cron_entry:
+      continue
+
+    curly_brace_count += line.count('{') - line.count('}')
+    if curly_brace_count == 0:
+      within_cron_entry = False
+      cron_entries.append(cron_entry)
+      cron_entry = {}
+      continue
+
+    if '=>' in line:
+      attribute, value = line.split('=>', 1)
+      if value.rstrip().endswith(','):
+        value = value.rstrip()[:-1]
+      cron_entry[attribute.strip()] = value.lstrip()
+
+  return cron_entries
+
+
+def cron_errors_to_comments(errors):
+  """Converts |errors| into Gerrit-style comments.
+
+  Args:
+    errors: List of (filename, cron_entry dict)
+  Returns:
+    Dict of Gerrit comments:
+      { 'filename': [{'line': '27', 'message': 'Error!'}, ...], ... }
+  """
+  comments = collections.defaultdict(list)
+  for puppet_file, cron_entry in errors:
+    comment = {'line': cron_entry['_line_number'],
+               'message': 'cron entry missing "minute" attribute.',}
+    comments[puppet_file].append(comment)
+  return comments
+
+
diff --git a/cq/scripts/helpers/puppet_utils_test.py b/cq/scripts/helpers/puppet_utils_test.py
new file mode 100644
index 0000000..1e66809
--- /dev/null
+++ b/cq/scripts/helpers/puppet_utils_test.py
@@ -0,0 +1,283 @@
+"""Tests for puppet_utils."""
+
+from __future__ import absolute_import
+import os
+import sys
+import unittest
+# Allow imports from cq/scripts folder
+sys.path.insert(
+    0, os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir)))
+
+from helpers import puppet_utils
+
+
+class PuppetParserUtilsTest(unittest.TestCase):
+  """Tests for the puppet parser utils."""
+
+  def testEmptyErrors(self):
+    """Tests converting 0 parser errors to comments."""
+    errors = []
+    directory = ''
+    expected_comments = {}
+    actual_comments = puppet_utils.parser_errors_to_comments(errors, directory)
+    self.assertEqual(actual_comments, expected_comments)
+
+  def testSingleError(self):
+    """Tests converting a single parser error to a comment."""
+    error = ("Error: Could not parse for environment production: "
+             "Syntax error at '='; expected '}' at "
+             "/abs/path/to/project/dir/manifests/common.pp:43")
+    errors = [error]
+    directory = '/abs/path/to/project/dir'
+    expected_comments = {
+        'manifests/common.pp': [{
+            'line': '43',
+            'message':
+                "Error: Could not parse for environment production: Syntax "
+                "error at '='; expected '}' at manifests/common.pp:43"
+        }]
+    }
+    actual_comments = puppet_utils.parser_errors_to_comments(errors, directory)
+    self.assertEqual(actual_comments, expected_comments)
+
+  def testSingleError_WithAnsiColorSequence(self):
+    """Tests stripping ANSI color sequences when converint to comments."""
+    error = ("\x1b[1;31mError: Could not parse for environment production: "
+             "Syntax error at '='; expected '}' at "
+             "/abs/path/to/project/dir/manifests/common.pp:43\x1b[1;39m")
+    errors = [error]
+    directory = '/abs/path/to/project/dir'
+    expected_comments = {
+        'manifests/common.pp': [{
+            'line': '43',
+            'message':
+                "Error: Could not parse for environment production: Syntax "
+                "error at '='; expected '}' at manifests/common.pp:43"
+        }]
+    }
+    actual_comments = puppet_utils.parser_errors_to_comments(errors, directory)
+    self.assertEqual(actual_comments, expected_comments)
+
+  def testMultipleErrors(self):
+    """Tests converting multiple parser errors into separate comments."""
+    error1 = ("Error: Could not parse for environment production: "
+              "Syntax error at '='; expected '}' at "
+              "/abs/path/to/project/dir/manifests/common.pp:43")
+    error2 = ("Error: Could not parse for environment production: "
+              "Syntax error at ','; expected ']' at "
+              "/abs/path/to/project/dir/manifests/other.pp:1")
+    errors = [error1, error2]
+    directory = '/abs/path/to/project/dir'
+    expected_comments = {
+        'manifests/common.pp': [{
+            'line': '43',
+            'message':
+                "Error: Could not parse for environment production: Syntax "
+                "error at '='; expected '}' at manifests/common.pp:43"
+        }],
+        'manifests/other.pp': [{
+            'line': '1',
+            'message':
+                "Error: Could not parse for environment production: Syntax "
+                "error at ','; expected ']' at manifests/other.pp:1"
+        }]
+    }
+    actual_comments = puppet_utils.parser_errors_to_comments(errors, directory)
+    self.assertEqual(actual_comments, expected_comments)
+
+
+class PuppetLinterUtilsTest(unittest.TestCase):
+  """Tests for puppet-lint utils."""
+
+  def testEmpty(self):
+    """Tests converting 0 lint errors into comments."""
+    errors = []
+    directory = ''
+    expected_comments = {}
+    actual_comments = puppet_utils.linter_errors_to_comments(errors, directory)
+    self.assertEqual(actual_comments, expected_comments)
+
+  def testSingleError(self):
+    """Tests converting a single linter error into a comment."""
+    errors = [{
+        'kind': 'error',
+        'check': 'syntax',
+        'message': 'Bad indentation.',
+        'line': 34,
+        'column': 10,
+        'fullpath': '/full/path/to/project/dir/manifests/common.pp',
+        'path': 'project/dir/manifests/common.pp',
+        'filename': 'common.pp',
+        'KIND': 'ERROR'
+    }]
+    directory = 'project/dir'
+    expected_comments = {
+        'manifests/common.pp': [{
+            'line': '34',
+            'message': 'Bad indentation.'
+        }]
+    }
+    actual_comments = puppet_utils.linter_errors_to_comments(errors, directory)
+    self.assertEqual(actual_comments, expected_comments)
+
+  def testMultipleErrors(self):
+    """Tests converting multiple linter errors into separate comments."""
+    errors = [{
+        'kind': 'error',
+        'check': 'syntax',
+        'message': 'Bad indentation.',
+        'line': 34,
+        'column': 10,
+        'fullpath': '/full/path/to/project/dir/manifests/common.pp',
+        'path': 'project/dir/manifests/common.pp',
+        'filename': 'common.pp',
+        'KIND': 'ERROR'
+    }, {
+        'message': 'unquoted resource title',
+        'line': 6,
+        'column': 11,
+        'token': '#<PuppetLint::Lexer::Token:0x00000001088350>',
+        'kind': 'warning',
+        'check': 'unquoted_resource_title',
+        'fullpath': '/path/to/project/dir/manifests/site.pp',
+        'path': 'project/dir/manifests/site.pp',
+        'filename': 'site.pp',
+        'KIND': 'WARNING'
+    }]
+    directory = 'project/dir'
+    expected_comments = {
+        'manifests/common.pp': [{
+            'line': '34',
+            'message': 'Bad indentation.'
+        }],
+        'manifests/site.pp': [{
+            'line': '6',
+            'message': 'unquoted resource title'
+        }]
+    }
+    actual_comments = puppet_utils.linter_errors_to_comments(errors, directory)
+    self.assertEqual(actual_comments, expected_comments)
+
+
+  def testErrorInNestedList(self):
+    """Tests that if errors are wrapped in a list, things still work."""
+    errors = [[{
+        'kind': 'error',
+        'check': 'syntax',
+        'message': 'Bad indentation.',
+        'line': 34,
+        'column': 10,
+        'fullpath': '/full/path/to/project/dir/manifests/common.pp',
+        'path': 'project/dir/manifests/common.pp',
+        'filename': 'common.pp',
+        'KIND': 'ERROR'
+    }]]
+    directory = 'project/dir'
+    expected_comments = {
+        'manifests/common.pp': [{
+            'line': '34',
+            'message': 'Bad indentation.'
+        }]
+    }
+    actual_comments = puppet_utils.linter_errors_to_comments(errors, directory)
+    self.assertEqual(actual_comments, expected_comments)
+
+
+class PuppetCronUtilsTest(unittest.TestCase):
+
+  def testGetCronEntries_EmptyFile(self):
+    puppet_lines = []
+    expected_cron_entries = []
+    actual_cron_entries = puppet_utils.get_cron_entries(puppet_lines)
+    self.assertEqual(expected_cron_entries, actual_cron_entries)
+
+  def testGetCronEntries_SingleCron(self):
+    puppet_lines = ['cron { "$cron_name":',
+                    '  ensure  => present,',
+                    '  command => $my_script,',
+                    '  hour    => 2,',
+                    '  minute  => 0,',
+                    '}']
+    expected_cron_entries = [
+        {'_line_number': '1',
+         'ensure': 'present',
+         'command': '$my_script',
+         'hour': '2',
+         'minute': '0',}
+    ]
+    actual_cron_entries = puppet_utils.get_cron_entries(puppet_lines)
+    self.assertEqual(expected_cron_entries, actual_cron_entries)
+
+  def testGetCronEntries_MultipleCrons(self):
+    puppet_lines = ['cron { "$cron_name":',
+                    '  ensure  => present,',
+                    '  command => $my_script,',
+                    '  hour    => 2,',
+                    '  minute  => 0,',
+                    '}',
+                    '',
+                    'cron { \'other_cron\':',
+                    '  ensure  => absent,',
+                    '  command => "/usr/bin/bad_thing arg1 arg2,',
+                    '  hour    => 2,',
+                    '}']
+    expected_cron_entries = [
+        {'_line_number': '1',
+         'ensure': 'present',
+         'command': '$my_script',
+         'hour': '2',
+         'minute': '0',},
+        {'_line_number': '8',
+         'ensure': 'absent',
+         'command': '"/usr/bin/bad_thing arg1 arg2',
+         'hour': '2',},
+    ]
+    actual_cron_entries = puppet_utils.get_cron_entries(puppet_lines)
+    self.assertEqual(expected_cron_entries, actual_cron_entries)
+
+
+  def testGetCronEntries_MultilineAttributeValue(self):
+    puppet_lines = ['cron { "$cron_name":',
+                    '  ensure  => present,',
+                    '  command => "/usr/bin/script ',
+                    '               --that-has-very-long-args=yep',
+                    '               --and-is-split-over-several-lints"',
+                    '  hour    => 2,',
+                    '  minute  => 0,',
+                    '}']
+    # The extra lines in the command attribute are just ignored.
+    expected_cron_entries = [
+        {'_line_number': '1',
+         'ensure': 'present',
+         'command': '"/usr/bin/script ',
+         'hour': '2',
+         'minute': '0',}
+    ]
+    actual_cron_entries = puppet_utils.get_cron_entries(puppet_lines)
+    self.assertEqual(expected_cron_entries, actual_cron_entries)
+
+  def testGetCronEntries_CurlyBraceCounting(self):
+    puppet_lines = ['cron { "$cron_name":',
+                    '  ensure  => present, ',
+                    '  command => "/usr/bin/script ',
+                    '  hour    => 2,',
+                    '  minute  => 0,',
+                    '  attr    => { nested stuff',
+                    '               xyz ',
+                    '             }',
+                    '}']
+    # The extra lines in the command attribute are just ignored.
+    expected_cron_entries = [
+        {'_line_number': '1',
+         'ensure': 'present',
+         'command': '"/usr/bin/script ',
+         'hour': '2',
+         'minute': '0',
+         'attr': '{ nested stuff',}
+    ]
+    actual_cron_entries = puppet_utils.get_cron_entries(puppet_lines)
+    self.assertEqual(expected_cron_entries, actual_cron_entries)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/py_env_utils.py b/cq/scripts/helpers/py_env_utils.py
new file mode 100644
index 0000000..966dde0
--- /dev/null
+++ b/cq/scripts/helpers/py_env_utils.py
@@ -0,0 +1,221 @@
+"""Script to help with the creation of python virtual envs."""
+import glob
+import logging
+import os
+from pathlib import Path
+import sys
+from typing import Iterable
+
+DEFAULT_LOCAL_PACKAGES = 'python_packages'
+VENV_RECIPE_PY2 = '~/venv_recipe/bin'
+
+
+def _get_pip(venv_root):
+  return os.path.join(venv_root, 'bin', 'pip')
+
+
+def add_import_paths(venv_root: Path, paths: Iterable[Path]) -> None:
+  """Add an entry into the custom import locations for a venv.
+
+  This will create a python path file in the provided virtual
+  environment such that python can import modules from
+  the provided path.
+
+  Args:
+    venv_root: The root directory of a virtualenv to install into.
+    paths: The paths to add
+  """
+  venv_lib = os.path.join(venv_root, 'lib', '*')
+  for version_lib in glob.glob(venv_lib):
+    pathfile = os.path.join(version_lib, 'site-packages', 'google.pth')
+    try:
+      with open(pathfile, 'r') as f:
+        content = f.read()
+      old_paths = [Path(p) for p in content.split('\n') if p]
+      new_paths = sorted(old_paths + paths)
+    except IOError:
+      new_paths = paths
+    with open(pathfile, 'w') as f:
+      f.write('\n'.join(str(p) for p in new_paths))
+    logging.info('Wrote the following paths to %s: %s', pathfile, paths)
+
+
+def create_virtual_env(
+    executor, directory, gcs_dir, local_packages=None, py3=False):
+  """Creates a python virtual env.
+
+  Args:
+    executor: Executes the subprocess.
+    directory: Directory to execute the subprocess in.
+    gcs_dir: The directory that mirrors the gcs bucket
+    local_packages: Relative path to python packages under ~/gcs-mirror
+    py3: if True use python3
+
+  Returns:
+    The path to the new virtualenv directory root.
+  """
+  python_sandbox_dir = os.path.join(directory, 'python-sandbox')
+  env = {}
+  if py3:
+    cmd = ['python3', '-m', 'venv', python_sandbox_dir]
+  else:
+    cmd = ['python2', '-m', 'virtualenv',
+           '--extra-search-dir={}'.format(
+               _get_find_links_path(gcs_dir, local_packages)),
+           '--no-download',
+           python_sandbox_dir]
+    env['PATH'] = os.pathsep.join([
+        os.path.expanduser(VENV_RECIPE_PY2), os.getenv('PATH')])
+  executor.exec_subprocess(cmd, env=env, check_output=True)
+  return python_sandbox_dir
+
+
+def install_package(executor, venv_root, path, gcs_dir, local_packages=None):
+  """Install a local package from disk.
+
+  Args:
+    executor: Executes the subprocess.
+    venv_root: The root directory of a virtualenv to install into.
+    path: The path to the package on local disk
+    gcs_dir: The path to the gcs mirror directory.
+    local_packages: Relative path to python packages under ~/gcs-mirror
+  """
+  # Perform necessary default package upgrades.
+  _update_default_deps(
+      executor,
+      venv_root,
+      ['pip', 'wheel', 'setuptools'],
+      local_packages,
+      gcs_dir
+  )
+
+  venv_pip = _get_pip(venv_root)
+
+  executor.exec_subprocess(
+      [venv_pip, 'install', '-e', path],
+      check_output=True)
+
+
+def install_py_deps(
+    executor, venv_root, requirements, gcs_dir, local_packages=None):
+  """Install the python dependencies.
+
+  Args:
+    executor: Executes the subprocess.
+    venv_root: The root directory of a virtualenv to install |requirements| in.
+    requirements: The path to the requirements.txt to install.
+    gcs_dir: The path to the gcs mirror directory.
+    local_packages: Relative path to python packages under ~/gcs-mirror
+  """
+  if not os.path.isfile(requirements):
+    logging.warning('No requirements file found: %s', requirements)
+    return
+
+  # Perform necessary default package upgrades.
+  _update_default_deps(
+      executor,
+      venv_root,
+      ['pip', 'wheel', 'setuptools'],
+      local_packages,
+      gcs_dir
+  )
+
+  venv_pip = _get_pip(venv_root)
+
+  executor.exec_subprocess(
+      [venv_pip, 'install', '--no-index',
+       '--find-links=file://{}'.format(
+           _get_find_links_path(gcs_dir, local_packages)),
+       '-r', requirements],
+      check_output=True)
+
+
+def _update_default_deps(executor, venv_root, deps, local_packages, gcs_dir):
+  """Update the pip packages required to install dependencies.
+
+  Args:
+    executor: Executes the subprocess.
+    venv_root: The root directory of the virtualenv.
+    deps: List of pip packages to update.
+    local_packages: Relative path to python packages under ~/gcs-mirror.
+    gcs_dir: The path to the gcs mirror directory.
+  """
+  venv_pip = _get_pip(venv_root)
+  for dep in deps:
+    executor.exec_subprocess(
+        [venv_pip, 'install', '--no-index',
+         '--find-links=file://{}'.format(
+             _get_find_links_path(gcs_dir, local_packages)),
+         '--upgrade', dep],
+        check_output=True)
+
+
+def exec_within_virtual_env(
+    executor, venv_root, cmd, env=None):
+  """Executes |cmd| within a python virtualenv.
+
+  Args:
+    executor: Executes the subprocess.
+    venv_root: The root directory of a virtualenv to run in.
+    cmd: The command list to pass to virtualenv python interpreter. This will
+        be prepended with virtualenv python, so it should not start with python.
+        (e.g. If you want to run `python my_python_script.py --flag1 --arg=3`
+        then pass cmd = ['my_python_script.py', '--flag1', '--arg=3'].)
+    env: A dictionary with additional environment variables.
+
+  Returns:
+    returncode, stdout, stderr from the subprocess.
+  """
+  venv_python = os.path.join(venv_root, 'bin', 'python')
+  full_cmd = [venv_python] + cmd
+  return executor.exec_subprocess(full_cmd, env=env)
+
+
+def _get_find_links_path(gcs_dir: str, local_packages=None):
+  if not local_packages:
+    local_packages = DEFAULT_LOCAL_PACKAGES
+  return os.path.expanduser(os.path.join(gcs_dir, local_packages))
+
+
+class ActivateVenv(object):
+  """Context manage to activate python virtual environment.
+
+  Will deactive on exit, and revert back to the previous venv.
+  This is designed to run from inside another venv. If not, it
+  will raise KeyError since sys.real_prefix only exists
+  when Python is run in a venv. Since our infras always run
+  from a venv, this is an acceptable assumption.
+
+  Args:
+    venv_root: Path to the python virtual env root.
+  """
+  def __init__(self, venv_root):
+    self._venv_root = venv_root
+    self._prev_os_path = None
+    self._prev_sys_path = None
+    self._prev_prefix = None
+    self._prev_real_prefix = None
+    self._save_current_env()
+
+  def __enter__(self):
+    env_activation_file = os.path.join(
+        self._venv_root, 'bin', 'activate_this.py')
+    exec(compile(open(env_activation_file, "rb").read(),
+                 env_activation_file, 'exec'),
+                 dict(__file__=env_activation_file))
+
+  def __exit__(self, *args):
+    sys.path[:0] = self._prev_sys_path
+    sys.prefix = self._prev_prefix
+    os.environ['PATH'] = self._prev_os_path
+    if self._prev_real_prefix is not None:
+      sys.real_prefix = self._prev_real_prefix
+
+  def _save_current_env(self):
+    self._prev_os_path = os.environ['PATH']
+    self._prev_sys_path = list(sys.path)
+    self._prev_prefix = sys.prefix
+    # real_prefix attribute only exists if Python is run from a venv.
+    if hasattr(sys, 'real_prefix'):
+      self._prev_real_prefix = sys.real_prefix
+
diff --git a/cq/scripts/helpers/py_test_runner.py b/cq/scripts/helpers/py_test_runner.py
new file mode 100644
index 0000000..0ab97d8
--- /dev/null
+++ b/cq/scripts/helpers/py_test_runner.py
@@ -0,0 +1,200 @@
+"""Run python unittests and dump a json report of the results to stdout.
+
+Usage:
+  python py_unittest_runner.py path/to/unittests_dir
+
+"""
+import argparse
+import json
+import logging
+import os
+from pathlib import Path
+import sys
+from typing import Optional
+
+import gunittest.runner
+
+LOGGER = logging.getLogger(__name__)
+
+def run_tests(directory, test_file_filter_regex, timeout,
+              fail_fast: bool,
+              after_module: Optional[str]=None,
+              multiprocessing=True, module_path_regex='.'):
+  """Crawl |directory| and run all python tests.
+
+  Args:
+    directory: Path to search for python tests to run.
+    test_file_filter_regex: File pattern for tests to be run.
+    timeout: Test timeout (in seconds).
+    multiprocessing: Use multiprocessing if True.
+    module_path_regex: Regex to determine which modules to include.
+    fail_fast: Fail on first encountered error.
+
+  Returns:
+    The summarized test results.
+  """
+
+  results = gunittest.runner.RunTests(
+      directory,
+      module_path_regex=module_path_regex,
+      test_file_filter_regex=test_file_filter_regex,
+      timeout=timeout,
+      use_multiprocessing=multiprocessing,
+      after_module=after_module,
+      fail_fast=fail_fast,
+  )
+  summary = gunittest.runner.Summarize(results)
+  return summary
+
+
+def generate_review(summary):
+  """Generate a Gerrit review from the test results |summary|.
+
+  Args:
+    summary: A dictionary with: errors, failures, results keys each
+        containing a list of db.model.TestResult objects, summarizing the
+        results of a test run.
+
+  Returns:
+    {'message': review_message, 'passing': passing}, where |review_message|
+    is a string giving summary information of the test run and |passing| is a
+    bool indicating if the tests all passed (no failurs, no errors).
+  """
+  errors = summary.get('errors', [])
+  failures = summary.get('failures', [])
+  all_results = summary.get('results', [])
+  elapsed_secs = summary.get('elapsed_secs', 0)
+  passing = len(errors) + len(failures) == 0
+  passing_msg = 'Passing' if passing else 'Failing'
+  message = ('Unit Test Results: {}\n'
+             'Run: {}, Errors: {}, Failures: {}\n'
+             'Total Run Time: {}').format(passing_msg,
+                                          len(all_results),
+                                          len(errors),
+                                          len(failures), elapsed_secs)
+  failure_msgs = [
+      '{}\n\n{}\n'.format(f.GetTestName(), f.GetStack()) for f in failures
+  ]
+  if failure_msgs:
+    message += '\n\nFailures:\n{}'.format('\n'.join(failure_msgs))
+
+  error_msgs = [
+      '{}\n\n{}\n'.format(e.GetTestName(), e.GetStack()) for e in errors
+  ]
+  if error_msgs:
+    message += '\n\nErrors:\n{}'.format('\n'.join(error_msgs))
+
+  LOGGER.info(message)
+
+  return {'message': message, 'passing': passing}
+
+
+def parse_cmdline_args() -> argparse.Namespace:
+  """Returns the parsed command line arguments."""
+  parser = argparse.ArgumentParser(
+      description='Run python tests in a given directory.')
+  parser.add_argument('--after-module',
+                      help=("Run tests that are from modules alphabetically "
+                            "after the provided term."))
+  parser.add_argument(
+      '--fail-fast',
+      action='store_true',
+      help='Fail on the first test error.')
+  parser.add_argument(
+      '--test_dir',
+      help='the directory to search for tests to run.',
+      required=True)
+  parser.add_argument(
+      '--output', type=Path, help='The file to output the JSON results to.', required=True)
+  parser.add_argument(
+      '--test_file_filter_regex',
+      help='File pattern for tests to be run.',
+      default='.*test.py$')
+  parser.add_argument(
+      '--module_path_regex',
+      help='Regex to determine which modules to include.',
+      default='.')
+  parser.add_argument(
+      '--timeout',
+      help='Timeout (in seconds) for running tests.',
+      type=int,
+      required=True)
+  parser.add_argument(
+      '--single-process',
+      help='Avoid using multiple processes when running tests.',
+      action='store_true',
+  )
+  parser.add_argument(
+      "--verbose",
+      action="store_true",
+      help="When present, show debug logs for the test runner.",
+  )
+  args = parser.parse_args()
+
+  if not os.path.isdir(args.test_dir):
+    parser.error(
+        'test_dir "{}" does not exist.'.format(os.path.abspath(args.test_dir)))
+
+  return args
+
+
+class LoggingSetupError(Exception):
+  pass
+
+
+def _setup_logging(verbose: bool) -> None:
+  """Set up logging.
+
+  We want to both allow sending logs from this runner and to capture logs
+  from tests that we plan to run.
+
+  Args:
+    verbose: show verbose logs
+  """
+  root = logging.getLogger()
+  if root.handlers:
+    raise LoggingSetupError(
+      "Someone is modifying the logging setup at import time. This is a bad "
+      "practice. This test runner refuses to fight with badly-behaved tests "
+      "and will now exit.")
+  root.setLevel(logging.WARNING)
+  level = logging.DEBUG if verbose else logging.INFO
+  LOGGER.setLevel(level)
+  stream = logging.StreamHandler()
+  stream.setFormatter(
+    logging.Formatter(
+      "%(levelname)s %(process)d:%(thread)d %(name)s %(asctime)s: %(message)s"))
+  LOGGER.addHandler(stream)
+  LOGGER.info("Logging handler established")
+  test_runner = logging.getLogger("gunittest.runner")
+  test_runner.addHandler(stream)
+  test_runner.setLevel(level)
+
+
+def main():
+  args = parse_cmdline_args()
+  try:
+    _setup_logging(args.verbose)
+  except LoggingSetupError as ex:
+    print(ex)
+    sys.exit(1)
+  test_results = run_tests(
+      args.test_dir,
+      test_file_filter_regex=args.test_file_filter_regex,
+      timeout=args.timeout,
+      multiprocessing=not args.single_process,
+      module_path_regex=args.module_path_regex,
+      after_module=args.after_module,
+      fail_fast=args.fail_fast,
+  )
+  review = generate_review(test_results)
+
+  if args.output.parent:
+    args.output.parent.mkdir(exist_ok=True)
+
+  with open(args.output, 'w') as output:
+    json.dump(review, output)
+
+
+if __name__ == '__main__':
+  main()
diff --git a/cq/scripts/helpers/pylint.py b/cq/scripts/helpers/pylint.py
new file mode 100755
index 0000000..85c0d82
--- /dev/null
+++ b/cq/scripts/helpers/pylint.py
@@ -0,0 +1,81 @@
+"""Script that lints a list of changed files then outputs a list of comments."""
+
+from __future__ import absolute_import
+import collections
+import logging
+import os
+import re
+import subprocess
+
+# path/to/file.py:27: [E1234(error-name), Text] Error message is here
+LINT_WARNING_PATTERN = (r'^(?P<filename>[^:]+):(?P<line_num>\d+): '
+                        r'\[(?P<error_code>\w+)\((?P<error_name>.*)\).*\] '
+                        r'(?P<error_msg>.*)$')
+
+
+class PylintError(Exception):
+  """Error for the pylint module."""
+
+
+def RunPylint(changed_files, pylintrc_file=None, cwd=None, executor=None):
+  """Runs pylint on a list of changed files.
+
+  Args:
+    changed_files: List of files to lint.
+    pylintrc_file: Path to a pylintrc file to use with pylint (or None to use
+        the default pylintrc from depot_tools).
+    cwd: If provided, cwd in which to run pylint.
+    executor: If provided, object with which to invoke pylint.
+
+  Returns:
+    stdout of the pylint command.
+
+  Raises:
+    PylintError: if pylint cannot be found.
+  """
+  if not changed_files:
+    return ''
+
+  pylint = os.path.expanduser('~/depot_tools/pylint')
+  if not os.path.exists(pylint):
+    logging.error('pylint not installed in expected location: %s', pylint)
+    raise PylintError('Could not find pylint.')
+
+  command = [pylint, '--output-format=parseable', '--disable=I']
+  if pylintrc_file:
+    command += ['--rcfile={}'.format(pylintrc_file)]
+  command += changed_files
+  if executor:
+    _, stdout, _ = executor.exec_subprocess(command, cwd=cwd)
+    return stdout
+
+  return subprocess.check_output(command, cwd=cwd)
+
+
+def ParseLint(lines, cwd=None):
+  """Reads lines from lint output and inserts them into comments.
+
+  Args:
+    lines: List of lint output lines to parse.
+    cwd: Base path relative to which parsed paths should be resolved.
+  Returns:
+    comments dictionary.
+  """
+  comments = collections.defaultdict(list)
+  for line in lines:
+    match = re.match(LINT_WARNING_PATTERN, line)
+    if match:
+      warning = {
+          'line':
+              match.group('line_num'),
+          'message':
+              '{} [{}]'.format(
+                  match.group('error_msg'), match.group('error_name'))
+      }
+      if cwd:
+        filename = os.path.relpath(match.group('filename'), cwd)
+      else:
+        filename = match.group('filename')
+      comments[filename].append(warning)
+
+  return comments
diff --git a/cq/scripts/helpers/pylint_test.py b/cq/scripts/helpers/pylint_test.py
new file mode 100644
index 0000000..cf39d1b
--- /dev/null
+++ b/cq/scripts/helpers/pylint_test.py
@@ -0,0 +1,40 @@
+"""Tests for pylint."""
+import os
+import sys
+import unittest
+
+from cq.scripts.helpers import pylint
+
+
+class PylintTest(unittest.TestCase):
+
+  def testParseLintOutput(self):
+    """Tests parsing lint output."""
+    py_file = 'path/to/file_one.py'
+    lines = [
+        '************* Module path.to.file_one',
+        'path/to/file_one.py:37: [E0001(error-1), RunPylint] First error.',
+        'path/to/file_one.py:50: [E(error/type2), RunPylint] Second error.',
+        'path/to/file_one.py:1: [W1201(warningXYZ), RunPylint] Warning.',
+    ]
+    result_comments = pylint.ParseLint(lines)
+
+    comments = result_comments[py_file]
+    self.assertIsNotNone(comments)
+    self.assertEqual(3, len(comments))
+
+    comment = comments[0]
+    self.assertEqual('37', comment['line'])
+    self.assertEqual('First error. [error-1]', comment['message'])
+
+    comment = comments[1]
+    self.assertEqual('50', comment['line'])
+    self.assertEqual('Second error. [error/type2]', comment['message'])
+
+    comment = comments[2]
+    self.assertEqual('1', comment['line'])
+    self.assertEqual('Warning. [warningXYZ]', comment['message'])
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/rbe_utils.py b/cq/scripts/helpers/rbe_utils.py
new file mode 100644
index 0000000..7557eb5
--- /dev/null
+++ b/cq/scripts/helpers/rbe_utils.py
@@ -0,0 +1,104 @@
+""" Utility functions for working with re-client tooling.
+
+Functions for starting and stopping reproxy.
+"""
+
+from __future__ import absolute_import
+import os
+
+RBE_SERVICE='remotebuildexecution.googleapis.com:443'
+DEFAULT_INSTANCE = 'projects/nest-rbe-prod/instances/us-central'
+RBE_REPROXY_ADDR = 'unix:///tmp/reproxy.sock'
+
+NEW_RBE_REL_PATH = 'chromium/src/buildtools/reclient'
+OLD_RBE_REL_PATH = 'chromium/src/buildtools/rbe'
+
+
+def _rbe_path():
+  """Path to RBE binaries relative to chromium/src
+
+  The path was moved from rbe -> reclient, but some old branches still use
+  the old path, so test the new path and fall back to the old path if needed.
+  """
+  if os.path.exists(NEW_RBE_REL_PATH):
+    return NEW_RBE_REL_PATH
+  return OLD_RBE_REL_PATH
+
+def _depscan_path():
+  return os.path.join(_rbe_path(), 'dependency_scanner_go_plugin.so')
+
+def _rbe_start_command():
+  """Command to start the remote execution proxy (reproxy)."""
+  return [os.path.join(_rbe_path(), 'bootstrap'),
+          '-re_proxy=' + os.path.join(_rbe_path(), 'reproxy')]
+
+def _rbe_stop_command():
+  """Command to stop the remote execution proxy (reproxy, via bootstrap)."""
+  return [os.path.join(_rbe_path(), 'bootstrap'),
+          '--shutdown']
+
+def is_installed():
+  """Returns True if the rbe bootstrap client is available."""
+  return os.path.exists(os.path.join(_rbe_path(), 'bootstrap'))
+
+
+def start_reproxy(executor):
+  """Runs 'reproxy <args>' to start the reproxy and connect to the rbe backend.
+
+  This is run in a non-blocking fashion as the reproxy should be running for
+  the duration of the receipe.
+
+  Args:
+    executor: Executes the subprocess.
+
+  Returns:
+    (returncode, stdout, stderr)
+  """
+  return executor.exec_subprocess(_rbe_start_command(),
+                                  env=_get_rbe_env(executor))
+
+
+def stop_reproxy(executor):
+  """Runs 'bootstrap --shutdown' which stops the reproxy process.
+
+  Args:
+    executor: Executes the subprocess.
+
+  Returns:
+    (returncode, stdout, stderr)
+  """
+  return executor.exec_subprocess(_rbe_stop_command(),
+                                  env=_get_rbe_env(executor))
+
+
+def _get_rbe_env(executor):
+  """Returns the dict of environment vars to pass to bootstrap/reproxy.
+
+  Args:
+    tmp_dir: Temporary directory to place logs.
+
+  Returns:
+    dict of environment variables.
+  """
+  # Set flags to reproxy via env variables.
+  tmp_dir = executor.get_tmp_dir()
+  log_dir = executor.get_gcs_dir()
+  env_values = {
+      'RBE_use_gce_credentials': 'true',
+      'RBE_service': RBE_SERVICE,
+      'RBE_instance': executor.get_property('rbe_instance', DEFAULT_INSTANCE),
+      'RBE_server_address': RBE_REPROXY_ADDR,
+      'RBE_clean_include_paths': 'true',
+      'RBE_log_dir': log_dir,
+      # The files generated by the proxy are large and not very useful.
+      'RBE_proxy_log_dir': tmp_dir,
+      'RBE_output_dir': log_dir,
+      # Set the clang plugins to ignore during deps scanning.
+      'RBE_clang_depscan_ignored_plugins': 
+        'find-bad-constructs,blink-gc-plugin',
+  }
+  # The dependency_scanner_go_plugin.so is removed in newer versions of RBE
+  # But older branches still need it. Include only if needed.
+  if os.path.exists(_depscan_path()):
+    env_values['RBE_cpp_dependency_scanner_plugin'] = _depscan_path()
+  return env_values
diff --git a/cq/scripts/helpers/repo_utils.py b/cq/scripts/helpers/repo_utils.py
new file mode 100644
index 0000000..d10a2b0
--- /dev/null
+++ b/cq/scripts/helpers/repo_utils.py
@@ -0,0 +1,497 @@
+"""Tools to interact with repo."""
+
+from __future__ import absolute_import
+from contextlib import contextmanager
+import logging
+import os
+import shutil
+import tempfile
+from xml.dom import minidom
+import xml.etree.ElementTree as et
+
+import six
+
+DEFAULT_REMOTE = 'eureka'
+
+EUREKA_MANIFEST_PROJECT_NAME = 'eureka/manifest'
+LIBASSISTANT_MANIFEST_PROJECT_NAME = 'standalone/manifest'
+
+EUREKA_INTERNAL_HOST_URL = 'https://eureka-internal.googlesource.com/'
+EUREKA_PARTNER_HOST_URL = 'https://eureka-partner.googlesource.com/'
+
+EUREKA_MANIFEST_URL = (
+    EUREKA_INTERNAL_HOST_URL + EUREKA_MANIFEST_PROJECT_NAME)
+CATABUILDER_E2E_TESTS_MANIFEST_URL = (
+    'https://eureka-staging-internal.googlesource.com/'
+    'catabuilder_e2e_tests_manifest')
+GOTHAM_MCU_MANIFEST_URL = (
+    'https://team.googlesource.com/gotham-cr/manifest')
+LIBASSISTANT_MANIFEST_URL = (
+    'https://libassistant-internal.googlesource.com/' +
+    LIBASSISTANT_MANIFEST_PROJECT_NAME)
+PARTNER_MANIFEST_URL = (
+    EUREKA_PARTNER_HOST_URL + 'partner/manifest')
+PARTNER_AMLOGIC_MANIFEST_URL = (
+    EUREKA_PARTNER_HOST_URL + 'amlogic/manifest')
+CAST_AUDIO_STANDARD_MANIFEST_URL = (
+    EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_audio')
+CAST_AUDIO_REFERENCE_MANIFEST_URL = (
+    EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_audio_reference')
+CAST_ASSISTANT_REFERENCE_MANIFEST_URL = (
+    EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_assistant_reference')
+CAST_TV_MANIFEST_URL = (
+    EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_tv')
+CAST_TV_MARVELL_MANIFEST_URL = (
+    EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_tv_marvell')
+CAST_TV_MEDIATEK_MANIFEST_URL = (
+    EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_tv_mediatek')
+CAST_TV_MSTAR_MANIFEST_URL = (
+    EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_tv_mstar')
+CAST_TV_NOVATEK_URL = (
+    EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_tv_novatek')
+CAST_TV_COMCAST_URL = (
+    EUREKA_PARTNER_HOST_URL + 'partner/manifest_comcast')
+ABBEY_MANIFEST_URL = (
+    EUREKA_INTERNAL_HOST_URL + 'abbey/manifest')
+PARTNER_QUALCOMM_MANIFEST_URL = (
+    EUREKA_PARTNER_HOST_URL + 'qualcomm/manifest')
+PARTNER_QUALCOMM_MANIFEST_OTA_URL = (
+    EUREKA_PARTNER_HOST_URL + 'qualcomm/manifest_ota')
+
+REPO_MANIFEST_PROJECT_PATH = '.repo/manifests'
+REPO_MANIFEST_PROJECT_REMOTE = 'origin'
+
+REPO_MANIFEST_URL_TO_PROJECT_NAME = {
+    EUREKA_MANIFEST_URL: EUREKA_MANIFEST_PROJECT_NAME,
+    LIBASSISTANT_MANIFEST_URL: LIBASSISTANT_MANIFEST_PROJECT_NAME,
+}
+
+MANIFEST_OVERRIDE_XML = 'manifest_contour.xml'
+
+REPO_MANIFEST_URL_TO_REPO_ALIAS = {
+    ABBEY_MANIFEST_URL: 'abbey',
+    CAST_ASSISTANT_REFERENCE_MANIFEST_URL: 'cast_assistant_reference',
+    CAST_AUDIO_REFERENCE_MANIFEST_URL: 'cast_audio_reference',
+    CAST_AUDIO_STANDARD_MANIFEST_URL: 'cast_audio_new',
+    CAST_TV_COMCAST_URL: 'cast_tv_comcast',
+    CAST_TV_MANIFEST_URL: 'cast_tv',
+    CAST_TV_MARVELL_MANIFEST_URL: 'cast_tv_marvell',
+    CAST_TV_MEDIATEK_MANIFEST_URL: 'cast_tv_mediatek',
+    CAST_TV_MSTAR_MANIFEST_URL: 'cast_tv_mstar',
+    CAST_TV_NOVATEK_URL: 'cast_tv_novatek',
+    EUREKA_MANIFEST_URL: 'internal',
+    GOTHAM_MCU_MANIFEST_URL: 'gotham_mcu',
+    LIBASSISTANT_MANIFEST_URL: 'libassistant',
+    PARTNER_AMLOGIC_MANIFEST_URL: 'partner_amlogic',
+    PARTNER_MANIFEST_URL: 'partner',
+    PARTNER_QUALCOMM_MANIFEST_URL: 'partner_qualcomm',
+    PARTNER_QUALCOMM_MANIFEST_OTA_URL: 'partner_qualcomm_ota',
+    CATABUILDER_E2E_TESTS_MANIFEST_URL: 'e2e_tests',
+}
+
+
+class DuplicateLookupError(Exception):
+  """Error if the project was included more than once."""
+
+
+class ManifestParseError(Exception):
+  """Error if unable to parse output of 'repo manifest' command."""
+
+
+class ProjectLookupTable(object):
+  """A lookup table for project and project paths."""
+
+  def __init__(self, lookup_dict=None, duplicate_keys=None):
+    """Create a ProjectLookupTable.
+
+    Args:
+      lookup_dict: Dictionary of key/value pairs.
+      duplicate_keys: A list of keys that were duplicates.
+    """
+    if not lookup_dict:
+      lookup_dict = {}
+    if not duplicate_keys:
+      duplicate_keys = []
+    self._lookup_dict = lookup_dict
+    self._duplicate_keys = duplicate_keys
+
+  def __getitem__(self, key):
+    """Get the value for the given key.
+
+    Args:
+      key: The key to get a value for.
+
+    Returns:
+      The value for the given key.
+
+    Raises:
+      KeyError: If the key is not included.
+      DuplicateLookupError: If the key was included multiple times.
+    """
+    return self.get(key)
+
+  def __setitem__(self, key, value):
+    """Sets the value for the given key."""
+    if self._lookup_dict.get(key):
+      self._duplicate_keys.append(key)
+    self._lookup_dict[key] = value
+
+  def __contains__(self, key):
+    """Returns true if key is in the underlying dict."""
+    return key in self._lookup_dict
+
+  def get(self, key):
+    """Get the value for the given key.
+
+    Args:
+      key: The key to get a value for.
+
+    Returns:
+      The value for the given key.
+
+    Raises:
+      KeyError: If the key is not included.
+      DuplicateLookupError: If the key was included multiple times.
+    """
+    if key in self._duplicate_keys:
+      raise DuplicateLookupError(
+          'Duplicate of {} found in manifest.'.format(key))
+
+    if key in self._lookup_dict:
+      return self._lookup_dict[key]
+
+    if key.endswith('/') and key[:-1] in self._lookup_dict:
+      return self._lookup_dict[key[:-1]]
+
+    # For legacy reasons, some project names in the manifest include a '.git'
+    # suffix. Gerrit strips off this suffix, though, so if we haven't found the
+    # key yet, try searching with a .git suffix.
+    key_git = key + '.git'
+    if key_git in self._lookup_dict:
+      return self._lookup_dict[key_git]
+
+    raise KeyError('{} was not found in the project path lookup {}.'.format(
+        key, self._lookup_dict))
+
+  def get_dict(self):
+    """Gets the dictionary representing the lookup table.
+
+    Returns:
+      The underlying dict storage of this object.
+    """
+    return self._lookup_dict
+
+
+def get_project_path_lookup(executor, manifest_url, manifest=None, **kwargs):
+  """Returns a project path lookup table.
+
+  Using manifest apgument as a manifest content by default, if None
+  `repo manifest` will be invoked.
+
+  Note that if duplicate projects are included in the manifest only 1 will be
+  included in the lookup. This will be the latter of the duplicates listed via
+  the `repo manifest` command.
+
+  Args:
+    executor: Executes the subprocess.
+    manifest_url: The url where the repo manifest project is hosted.
+    manifest: Content of manifest file already loaded by build.
+    **kwargs: Any additional kwargs to pass to the executor.
+
+  Returns:
+    A ProjectLookupTable representing the relative path for the given project.
+  """
+  if not manifest:
+    manifest = get_manifest(executor, **kwargs)
+  return parse_manifest_for_path(manifest, manifest_url)
+
+
+@contextmanager
+def normalize_manifest_if_exists(manifest_path):
+  """Normalizes the manifest for the duration of the context manager."""
+  if manifest_path:
+    temp_manifest = tempfile.NamedTemporaryFile(delete='False').name
+    shutil.copy2(manifest_path, temp_manifest)
+    _normalize_manifest(manifest_path)
+  try:
+    yield
+  finally:
+    if manifest_path:
+      shutil.copy2(temp_manifest, manifest_path)
+      os.unlink(temp_manifest)
+
+
+def _normalize_manifest(manifest_path):
+  """Convert manifest to a more user friendly version."""
+  with open(manifest_path, 'r') as f:
+    manifest = ''.join([
+        line.strip() for line in f.readlines()])
+    root = et.fromstring(manifest)
+    for remote in root.findall('remote'):
+      if 'github.googlesource.com' in remote.get('fetch'):
+        remote.set('fetch', 'https://github.com')
+        remote.attrib.pop('review', None)
+        break
+  normalized_manifest = minidom.parseString(
+      et.tostring(root)).toprettyxml(encoding='UTF-8', indent='  ')
+  with open(manifest_path, 'wb') as f:
+    f.write(normalized_manifest)
+
+
+def override_manifest_command(override_path=None):
+  """Returns command for overriding .repo/manifest.xml by override_path."""
+  if override_path is None:
+    override_path = os.path.join(os.pardir, MANIFEST_OVERRIDE_XML)
+
+  return ['ln', '-sf', override_path, 'manifest.xml']
+
+
+def override_manifest(executor, override_path=None):
+  """Overriding .repo/manifest.xml by override_path."""
+  return executor.exec_subprocess(
+      override_manifest_command(override_path), cwd='.repo')
+
+
+def get_project_remote_lookup(executor, manifest_url, manifest=None, **kwargs):
+  """Returns a project remote lookup table.
+
+  Using manifest apgument as a manifest content by default, if None
+  `repo manifest` will be invoked.
+
+  Note that if duplicate projects are included in the manifest only 1 will be
+  included in the lookup. This will be the latter of the duplicates listed via
+  the `repo manifest` command.
+
+  Args:
+    executor: Executes the subprocess.
+    manifest_url: The url where the repo manifest project is hosted.
+    manifest: Content of manifest file already loaded by build.
+    **kwargs: Any additional kwargs to pass to the executor.
+
+  Returns:
+    A ProjectLookupTable representing the remote for the given project.
+  """
+  if not manifest:
+    manifest = get_manifest(executor, **kwargs)
+  return parse_manifest_for_remote(manifest, manifest_url)
+
+
+def get_project_revision_lookup(executor, manifest_url, manifest_branch,
+                                manifest=None, **kwargs):
+  """Returns a project revision lookup table.
+
+  Using manifest apgument as a manifest content by default, if None
+  `repo manifest` will be invoked.
+
+  Note that if duplicate projects are included in the manifest only 1 will be
+  included in the lookup. This will be the latter of the duplicates listed via
+  the `repo manifest` command.
+
+  Args:
+    executor: Executes the subprocess.
+    manifest_url: The url where the repo manifest project is hosted.
+    manifest_branch: The repo manifest branch of the current checkout.
+    manifest: Content of manifest file already loaded by build.
+    **kwargs: Any additional kwargs to pass to the executor.
+
+  Returns:
+    A ProjectLookupTable representing the revision for the given project.
+  """
+  if not manifest:
+    manifest = get_manifest(executor, **kwargs)
+  return parse_manifest_for_revision(manifest, manifest_url, manifest_branch)
+
+
+def get_manifest(executor, retries=5, **kwargs):
+  """Returns the manifest file in a string form.
+
+  Args:
+    executor: Executes the subprocess.
+    retries: number of retries to load manifest.
+    **kwargs: Any additional kwargs to pass to the executor.
+
+  Raises:
+    ManifestParseError: if all retries fail.
+  Returns:
+    A string representing the manifest file.
+  """
+  failures = 0
+  while retries > failures:
+    manifest = executor.exec_subprocess(
+        ['repo', 'manifest'],
+        check_output=True,
+        **kwargs)
+    try:
+      lines = ''.join([line.strip() for line in manifest.splitlines()])
+      _ = et.fromstring(lines)
+      return lines
+    except et.ParseError as error:
+      failures += 1
+      logging.warning(('Failed to parse manifest file. Attempt %d/%d. '
+                       'Error was: %s\nManifest file content was:\n%s'),
+                      failures, retries, error, manifest)
+
+  raise ManifestParseError(
+      'Failed to parse manifest after %s retries' % failures)
+
+
+def is_manifest_project(project):
+  """Checks if the project is a Manifest project.
+
+  Args:
+    project: The name of the Gerrit project in which the files live.
+
+  Returns:
+    True if it is a Manifest, False if not.
+  """
+  return project in list(REPO_MANIFEST_URL_TO_PROJECT_NAME.values())
+
+
+def parse_manifest_for_path(manifest, manifest_url):
+  """Parses a repo manifest file for project paths.
+
+  Args:
+    manifest: A string representing a manifest file.
+    manifest_url: The url where the repo manifest project is hosted.
+
+  Returns:
+    A ProjectLookupTable representing the manifest for the given project.
+  """
+  assert isinstance(manifest, six.string_types)
+  result = ProjectLookupTable()
+
+  # Add an entry to this map representing the repo manifest project.
+  manifest_project_name = get_manifest_url_mapping(
+      REPO_MANIFEST_URL_TO_PROJECT_NAME, manifest_url)
+  if manifest_project_name:
+    result[manifest_project_name] = REPO_MANIFEST_PROJECT_PATH
+
+  if not manifest:
+    return result
+  manifest = ''.join([line.strip() for line in manifest.splitlines()])
+  root = et.fromstring(manifest)
+  for project in root.findall('project'):
+    name = project.get('name')
+    path = project.get('path')
+    if not path:
+      path = name
+    result[name] = path
+  return result
+
+
+def parse_manifest_for_remote(manifest, manifest_url):
+  """Parses a repo manifest file for project remotes.
+
+  Args:
+    manifest: A string representing a manifest file.
+    manifest_url: The url where the repo manifest project is hosted.
+
+  Returns:
+    A ProjectLookupTable representing the manifest for the given project.
+  """
+  assert isinstance(manifest, six.string_types)
+  result = ProjectLookupTable()
+
+  # Add an entry to this map representing the repo manifest project.
+  manifest_project_name = get_manifest_url_mapping(
+      REPO_MANIFEST_URL_TO_PROJECT_NAME, manifest_url)
+  if manifest_project_name:
+    result[manifest_project_name] = REPO_MANIFEST_PROJECT_REMOTE
+
+  if not manifest:
+    return result
+  manifest = ''.join([line.strip() for line in manifest.splitlines()])
+  root = et.fromstring(manifest)
+  default_remote = DEFAULT_REMOTE
+  n = root.find('default')
+  if n is not None and n.get('remote') is not None:
+    default_remote = n.get('remote')
+  for project in root.findall('project'):
+    name = project.get('name')
+    remote = project.get('remote')
+    if not remote:
+      remote = default_remote
+    result[name] = remote
+  return result
+
+
+def parse_manifest_for_revision(manifest, manifest_url, manifest_branch):
+  """Parses a repo manifest file for project revisions.
+
+  Args:
+    manifest: A string representing a manifest file.
+    manifest_url: The url where the repo manifest project is hosted.
+    manifest_branch: The repo manifest branch of the current checkout.
+
+  Returns:
+    A ProjectLookupTable representing the manifest for the given project.
+  """
+  assert isinstance(manifest, six.string_types)
+  result = ProjectLookupTable()
+
+  manifest_project_name = get_manifest_url_mapping(
+      REPO_MANIFEST_URL_TO_PROJECT_NAME, manifest_url)
+  if manifest_project_name:
+    result[manifest_project_name] = manifest_branch
+
+  if not manifest:
+    return result
+  manifest = ''.join([line.strip() for line in manifest.splitlines()])
+  root = et.fromstring(manifest)
+
+  # Get the default revision frome the manifest.
+  default_revision = ''
+  for default_tag in root.findall('default'):
+    default_revision = default_tag.get('revision', '')
+
+  # Create the lookup. If an entry does not explicitly reference a revision,
+  # assign the default revision.
+  for project in root.findall('project'):
+    name = project.get('name')
+    revision = project.get('revision')
+    if not revision:
+      revision = default_revision
+    result[name] = revision
+  return result
+
+
+def requires_repo_sync(project, filenames):
+  """Given a list of touched files, determines if a repo sync is needed.
+
+  If a change is applied to a repo manifest file (for example, to update the
+  revision of a project), we may need to run 'repo sync' again before building,
+  to ensure the project is checked out at the SHA we are trying to test. Return
+  true if this is the case.
+
+  Args:
+      project: The name of the Gerrit project in which the files live.
+      filenames: A list of files that were modified in project.
+  Returns:
+      True if we should run 'repo sync' again before building.
+
+  """
+
+  # Projects that are not manifest projects can be ignored.
+  if not is_manifest_project(project):
+    return False
+
+  # If any file has been touched, we need to repo sync.
+  return bool(filenames)
+
+
+def get_manifest_url_mapping(mapping, manifest_url):
+  """Get value from manifest_url mapping regardless of GoB domain name."""
+  return mapping.get(manifest_url.replace(
+      '.git.corp.google.com', '.googlesource.com'))
+
+
+def use_eureka_manifest(manifest_url):
+  """Returns True if this manifest points to eureka/manifest."""
+  project_name = get_manifest_url_mapping(
+      REPO_MANIFEST_URL_TO_PROJECT_NAME, manifest_url)
+  return project_name and project_name == EUREKA_MANIFEST_PROJECT_NAME
+
+
+def manifest_alias(manifest_url):
+  """Returns manifest alias."""
+  return get_manifest_url_mapping(REPO_MANIFEST_URL_TO_REPO_ALIAS, manifest_url)
diff --git a/cq/scripts/helpers/repo_utils_test.py b/cq/scripts/helpers/repo_utils_test.py
new file mode 100644
index 0000000..1eec759
--- /dev/null
+++ b/cq/scripts/helpers/repo_utils_test.py
@@ -0,0 +1,360 @@
+"""Tests for repo_utils."""
+
+from __future__ import absolute_import
+import os
+import sys
+import tempfile
+import unittest
+
+# Allow imports from cq/scripts folder
+sys.path.insert(
+    0,
+    os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir)))
+from helpers import repo_utils
+
+SAMPLE_MANIFEST_STRING = """
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest>
+  <remote fetch="https://chromium.googlesource.com/" name="chromium" review="https://chromium-review.googlesource.com/"/>
+  <remote fetch="https://eureka-internal.googlesource.com/" name="eureka" review="sso://eureka-internal"/>
+
+  <default revision="master"/>
+
+  <project name="chromium/src" remote="eureka" revision="unfork_m43"/>
+  <project name="chromiumos/chromite" path="chromite/chromite" remote="chromium"/>
+  <project name="chromiumos/third_party/autotest" path="autotest" remote="chromium"/>
+  <project name="continuous-tests" remote="eureka"/>
+  <project name="fragglerock" remote="eureka"/>
+  <project name="prebuilt/toolchain" path="toolchain" remote="eureka"/>
+  <project name="test" remote="eureka"/>
+  <project name="legacyname.git" path="path/to/legacy" remote="eureka"/>
+</manifest>
+"""
+
+SAMPLE_MANIFEST_STRING_WITH_DUPLICATE_NAMES = """
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest>
+  <remote fetch="https://chromium.googlesource.com/" name="chromium" review="https://chromium-review.googlesource.com/"/>
+  <remote fetch="https://eureka-internal.googlesource.com/" name="eureka" review="sso://eureka-internal"/>
+
+  <default revision="master"/>
+
+  <project name="chromium/src" remote="eureka" revision="unfork_m43"/>
+  <project name="chromiumos/chromite" path="chromite/chromite" remote="chromium"/>
+  <project name="chromiumos/third_party/autotest" path="autotest" remote="chromium"/>
+  <project name="continuous-tests" remote="eureka"/>
+  <project name="fragglerock" remote="eureka"/>
+  <project name="prebuilt/toolchain" path="toolchain" remote="eureka"/>
+  <project name="test" path="test1" remote="eureka"/>
+  <project name="test" path="test2" remote="eureka"/>
+</manifest>
+"""
+
+SAMPLE_MANIFEST_INCOMPLETE_STRING = """
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest>
+  <remote fetch="https://chromium.googlesource.com/" name="chromium" review="https://chromium-review.googlesource.com/"/>
+  <remote fetch="https://eureka-internal.googlesource.com/" name="eureka" review="sso://eureka-internal"/>
+
+  <default revision="master"/>
+
+  <project name="chromium/src" remote="eureka" revision="unfork_m43"/>
+  <project name="chromiumos/chromite" path="chromite/chromite" remote="chromium"/>
+  <project name="chromiumos/third_party/autotest" path="autotest" remote="chromium"/>
+  <project name="continuous-tests" remote="eureka"/>
+  <project name="fragglerock" remote="eureka"/>
+  <project name="prebuilt/toolchain" path="toolchain" remote="eureka"/>
+"""
+
+SAMPLE_MANIFEST_WITH_DEFAULT_REMOTE = """
+<manifest>
+  <remote fetch="https://nest-internal.googlesource.com" name="cq-manifest" review="https://nest-internal-review.googlesource.com/"/>
+  <default remote="cq-manifest" revision="master"/>
+  <project name="nep/nest-harness-tests" path="nep/nest-harness-tests" />
+</manifest>
+"""
+
+SAMPLE_MANIFEST_WITH_INTERNAL_GITHUB = """<manifest>
+  <remote fetch="https://eureka-internal.googlesource.com/" name="eureka" review="sso://eureka-internal"/>
+  <remote fetch="https://github.googlesource.com" name="github" review="https://github-review.googlesource.com/"/>
+  <remote fetch="https://chromium.googlesource.com/" name="chromium" review="https://chromium-review.googlesource.com/"/>
+  <default revision="master"/>
+  <project name="chromium/src" remote="eureka" revision="unfork_m43"/>
+  <project name="test-project1" remote="github"/>
+  <project name="chromiumos/chromite" path="chromite/chromite" remote="chromium"/>
+</manifest>
+"""
+
+SAMPLE_MANIFEST_WITH_EXTERNAL_GITHUB = """<?xml version="1.0" encoding="UTF-8"?>
+<manifest>
+  <remote fetch="https://eureka-internal.googlesource.com/" name="eureka" review="sso://eureka-internal"/>
+  <remote fetch="https://github.com" name="github"/>
+  <remote fetch="https://chromium.googlesource.com/" name="chromium" review="https://chromium-review.googlesource.com/"/>
+  <default revision="master"/>
+  <project name="chromium/src" remote="eureka" revision="unfork_m43"/>
+  <project name="test-project1" remote="github"/>
+  <project name="chromiumos/chromite" path="chromite/chromite" remote="chromium"/>
+</manifest>
+"""
+
+TEST_MANIFEST_URL = 'https://foo.bar.googlesource.com/bat/baz'
+
+
+class RepoUtilsTest(unittest.TestCase):
+  """Tests for the repo utils module."""
+
+  def testParseEmptyManifest(self):
+    """Tests parsing an empty manifest file."""
+    manifest = ''
+    expected_lookup = {
+        'eureka/manifest': '.repo/manifests',
+    }
+    project_lookup = repo_utils.parse_manifest_for_path(
+        manifest, repo_utils.EUREKA_MANIFEST_URL)
+    lookup = project_lookup._lookup_dict
+    self.assertEqual(expected_lookup, lookup)
+
+  def testParseManifest(self):
+    """Tests parsing a manifest file for paths."""
+    expected_lookup = {
+        'chromium/src': 'chromium/src',
+        'chromiumos/chromite': 'chromite/chromite',
+        'chromiumos/third_party/autotest': 'autotest',
+        'continuous-tests': 'continuous-tests',
+        'eureka/manifest': '.repo/manifests',
+        'fragglerock': 'fragglerock',
+        'legacyname.git': 'path/to/legacy',
+        'prebuilt/toolchain': 'toolchain',
+        'test': 'test'
+    }
+    project_lookup = repo_utils.parse_manifest_for_path(
+        SAMPLE_MANIFEST_STRING,
+        repo_utils.EUREKA_MANIFEST_URL)
+    lookup = project_lookup._lookup_dict
+    self.assertEqual(expected_lookup, lookup)
+
+  def testParseManifestForNonEurekaManifest(self):
+    """Tests parsing a non-eureka manifest for path."""
+    expected_lookup = {
+        'bat/baz': '.repo/manifests',
+        'chromium/src': 'chromium/src',
+        'chromiumos/chromite': 'chromite/chromite',
+        'chromiumos/third_party/autotest': 'autotest',
+        'continuous-tests': 'continuous-tests',
+        'fragglerock': 'fragglerock',
+        'legacyname.git': 'path/to/legacy',
+        'prebuilt/toolchain': 'toolchain',
+        'test': 'test'
+    }
+    original_map = repo_utils.REPO_MANIFEST_URL_TO_PROJECT_NAME
+    repo_utils.REPO_MANIFEST_URL_TO_PROJECT_NAME = {TEST_MANIFEST_URL:
+                                                    'bat/baz'}
+    project_lookup = repo_utils.parse_manifest_for_path(
+        SAMPLE_MANIFEST_STRING, TEST_MANIFEST_URL)
+    lookup = project_lookup._lookup_dict
+    self.assertEqual(expected_lookup, lookup)
+    repo_utils.REPO_MANIFEST_URL_TO_PROJECT_NAME = original_map
+
+  def testParseManifestRemotes(self):
+    """Tests parsing a manifest file for remotes."""
+    expected_lookup = {
+        'chromium/src': 'eureka',
+        'chromiumos/chromite': 'chromium',
+        'chromiumos/third_party/autotest': 'chromium',
+        'continuous-tests': 'eureka',
+        'eureka/manifest': 'origin',
+        'fragglerock': 'eureka',
+        'legacyname.git': 'eureka',
+        'prebuilt/toolchain': 'eureka',
+        'test': 'eureka'
+    }
+    project_lookup = repo_utils.parse_manifest_for_remote(
+        SAMPLE_MANIFEST_STRING,
+        repo_utils.EUREKA_MANIFEST_URL)
+    lookup = project_lookup._lookup_dict
+    self.assertEqual(expected_lookup, lookup)
+
+  def testParseManifestRemotesForNonEurekaManifest(self):
+    """Tests parsing a non-eureka manifest for remotes."""
+    expected_lookup = {
+        'bat/baz': 'origin',
+        'chromium/src': 'eureka',
+        'chromiumos/chromite': 'chromium',
+        'chromiumos/third_party/autotest': 'chromium',
+        'continuous-tests': 'eureka',
+        'fragglerock': 'eureka',
+        'legacyname.git': 'eureka',
+        'prebuilt/toolchain': 'eureka',
+        'test': 'eureka'
+    }
+    original_map = repo_utils.REPO_MANIFEST_URL_TO_PROJECT_NAME
+    repo_utils.REPO_MANIFEST_URL_TO_PROJECT_NAME = {TEST_MANIFEST_URL:
+                                                    'bat/baz'}
+    project_lookup = repo_utils.parse_manifest_for_remote(
+        SAMPLE_MANIFEST_STRING, TEST_MANIFEST_URL)
+    lookup = project_lookup._lookup_dict
+    self.assertEqual(expected_lookup, lookup)
+    repo_utils.REPO_MANIFEST_URL_TO_PROJECT_NAME = original_map
+
+  def testParseManifestRemotesForNonEurekaDefaultRemote(self):
+    expected_lookup = {
+        'nep/nest-harness-tests': 'cq-manifest'
+    }
+    project_lookup = repo_utils.parse_manifest_for_remote(
+        SAMPLE_MANIFEST_WITH_DEFAULT_REMOTE, TEST_MANIFEST_URL)
+    lookup = project_lookup._lookup_dict
+    self.assertEqual(expected_lookup, lookup)
+
+  def testParseManifestRevisions(self):
+    """Tests parsing a manifest file for revisions."""
+    expected_lookup = {
+        'chromium/src': 'unfork_m43',
+        'chromiumos/chromite': 'master',
+        'chromiumos/third_party/autotest': 'master',
+        'continuous-tests': 'master',
+        'eureka/manifest': 'unfork_m51',
+        'fragglerock': 'master',
+        'legacyname.git': 'master',
+        'prebuilt/toolchain': 'master',
+        'test': 'master'
+    }
+    project_lookup = repo_utils.parse_manifest_for_revision(
+        SAMPLE_MANIFEST_STRING,
+        repo_utils.EUREKA_MANIFEST_URL,
+        'unfork_m51')
+    lookup = project_lookup._lookup_dict
+    self.assertEqual(expected_lookup, lookup)
+
+  def testParseManifestWithDups(self):
+    """Tests parsing a manifest file with duplicates."""
+    expected_lookup = {
+        'chromium/src': 'chromium/src',
+        'chromiumos/chromite': 'chromite/chromite',
+        'chromiumos/third_party/autotest': 'autotest',
+        'continuous-tests': 'continuous-tests',
+        'eureka/manifest': '.repo/manifests',
+        'fragglerock': 'fragglerock',
+        'prebuilt/toolchain': 'toolchain',
+        'test': 'test2'
+    }
+    expected_duplicates = ['test']
+    project_lookup = repo_utils.parse_manifest_for_path(
+        SAMPLE_MANIFEST_STRING_WITH_DUPLICATE_NAMES,
+        repo_utils.EUREKA_MANIFEST_URL)
+    lookup = project_lookup._lookup_dict
+    duplicates = project_lookup._duplicate_keys
+    self.assertEqual(expected_lookup, lookup)
+    self.assertEqual(expected_duplicates, duplicates)
+
+  def testLookupDuplicateRepoRaisesError(self):
+    """Tests a lookup on a duplicate repo raises an error."""
+
+    class MockExecutor(object):
+
+      # pylint: disable=invalid-name,no-method-argument
+      def exec_subprocess(*args, **kwargs):
+        del kwargs
+        return SAMPLE_MANIFEST_STRING_WITH_DUPLICATE_NAMES
+
+    lookup = repo_utils.get_project_path_lookup(
+        MockExecutor(), repo_utils.EUREKA_MANIFEST_URL)
+
+    self.assertEqual('continuous-tests', lookup.get('continuous-tests'))
+    with self.assertRaises(KeyError):
+      lookup.get('foobar')
+    with self.assertRaises(repo_utils.DuplicateLookupError):
+      lookup.get('test')
+    self.assertEqual('continuous-tests', lookup['continuous-tests'])
+    with self.assertRaises(KeyError):
+      lookup['foobar']  # pylint: disable=pointless-statement
+    with self.assertRaises(repo_utils.DuplicateLookupError):
+      lookup['test']  # pylint: disable=pointless-statement
+
+  def testManifestParseError(self):
+    """Tests parsing an incorrect manifest file."""
+
+    class MockExecutor(object):
+
+      # pylint: disable=invalid-name,no-method-argument
+      def exec_subprocess(*args, **kwargs):
+        del kwargs
+        return SAMPLE_MANIFEST_INCOMPLETE_STRING
+
+    with self.assertRaises(repo_utils.ManifestParseError):
+      _ = repo_utils.get_manifest(MockExecutor())
+
+  def testEmptyChangeDoesNotRequireRepoSync(self):
+    """Tests that an empty commit does not require a sync."""
+    project = 'eureka/manifest'
+
+    files = []
+    self.assertFalse(repo_utils.requires_repo_sync(project, files))
+
+  def testSyncNotRequiredForOtherProjects(self):
+    """Tests that a sync is not required for other projects."""
+    project = 'third_party/foobar'
+
+    files = ['default.xml']
+    self.assertFalse(repo_utils.requires_repo_sync(project, files))
+
+  def testChangeToAnyManifestFileRequiresRepoSync(self):
+    """Tests that a change to any manifest requires a sync."""
+    project = 'eureka/manifest'
+
+    files = ['default.xml']
+    self.assertTrue(repo_utils.requires_repo_sync(project, files))
+
+    files = ['foobar.xml']
+    self.assertTrue(repo_utils.requires_repo_sync(project, files))
+
+  def testProjectLookupWithTrailingSlash(self):
+    """Tests searching by project with a trailing slash."""
+    expected_lookup = {
+        'chromium/src': 'chromium/src',
+        'chromiumos/chromite': 'chromite/chromite',
+        'chromiumos/third_party/autotest': 'autotest',
+        'continuous-tests': 'continuous-tests',
+        'eureka/manifest': '.repo/manifests',
+        'fragglerock': 'fragglerock',
+        'prebuilt/toolchain': 'toolchain',
+        'test': 'test'
+    }
+    project_lookup = repo_utils.parse_manifest_for_path(
+        SAMPLE_MANIFEST_STRING,
+        repo_utils.EUREKA_MANIFEST_URL)
+    for key, expected_value in expected_lookup.items():
+      self.assertEqual(expected_value, project_lookup.get(key))
+      self.assertEqual(expected_value, project_lookup.get(key + '/'))
+
+  def testProjectLookupWithGitSuffix(self):
+    """Tests finding a project name that has a .git suffix."""
+    key_without_git_suffix = 'legacyname'
+    key_with_git_suffix = 'legacyname.git'
+    expected_path = 'path/to/legacy'
+    project_lookup = repo_utils.parse_manifest_for_path(
+        SAMPLE_MANIFEST_STRING,
+        repo_utils.EUREKA_MANIFEST_URL)
+
+    actual_path = project_lookup.get(key_without_git_suffix)
+    self.assertEqual(expected_path, actual_path)
+
+    actual_path = project_lookup.get(key_with_git_suffix)
+    self.assertEqual(expected_path, actual_path)
+
+  def testNormalizeManifestSwapsOutInternalGithub(self):
+    """Tests github.com is temporarily used for the remote fetch."""
+    with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
+      f.write(SAMPLE_MANIFEST_WITH_INTERNAL_GITHUB)
+      temp_manifest = f.name
+    with repo_utils.normalize_manifest_if_exists(temp_manifest):
+      with open(temp_manifest, 'r') as f:
+        manifest_content = f.read()
+        self.assertEqual(manifest_content, SAMPLE_MANIFEST_WITH_EXTERNAL_GITHUB)
+    with open(temp_manifest, 'r') as f:
+      manifest_content = f.read()
+      self.assertEqual(manifest_content, SAMPLE_MANIFEST_WITH_INTERNAL_GITHUB)
+    os.unlink(temp_manifest)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/retry_utils.py b/cq/scripts/helpers/retry_utils.py
new file mode 100644
index 0000000..7eb2747
--- /dev/null
+++ b/cq/scripts/helpers/retry_utils.py
@@ -0,0 +1,29 @@
+import logging
+import random
+
+def disable_retry_wait_for_test():
+  """Disables retry waiting. Use only with tests."""
+  disable_retry_wait_for_test.on = True
+disable_retry_wait_for_test.on = False
+
+def retry_delay(attempt, min_delay=5, base=30):
+  """Get seconds to delay before attempting a retry.
+
+  Uses exponential backoff to determine the delay time. Setting min_delay
+  to 5s since lsremote and bandwidth refills every 5s.
+
+  Args:
+    attempt: current attempt number.
+    min_delay: left boundary of delay time to randomly choose from.
+    base: delay duration we are doubling on each attempt.
+
+  Returns:
+    The number of seconds to wait before retrying.
+  """
+  assert min_delay <= base
+  exponential_delay = base * 2**attempt
+  logging.info('Picking a random delay between '
+               '%s and %s seconds', min_delay, exponential_delay)
+  if disable_retry_wait_for_test.on:
+    return 0
+  return random.randint(min_delay, exponential_delay)
diff --git a/cq/scripts/helpers/sanitizer_utils.py b/cq/scripts/helpers/sanitizer_utils.py
new file mode 100644
index 0000000..e1a5745
--- /dev/null
+++ b/cq/scripts/helpers/sanitizer_utils.py
@@ -0,0 +1,199 @@
+"""Utils for working with the Clang sanitizers (asan, msan, tsan, ubsan)."""
+
+_SANITIZER_STRING = {
+    'asan': 'AddressSanitizer',
+    'lsan': 'LeakSanitizer',
+    'msan': 'MemorySanitizer',
+    'tsan': 'ThreadSanitizer',
+    'ubsan': 'runtime error:'
+}
+
+_MAX_ERRORS = 1
+_TOO_MANY_ERRORS_MSG = '<Too many errors to print. Check logs for more.>'
+_MAX_LINES_PER_ERROR = 30
+_ERROR_SNIPPED_MSG = '<...snip (error too long; removed {} lines)...>\n'
+
+
+def get_sanitizer_to_comments_for(sanitizer):
+  """Returns a function that converts |sanitizer| output to review comments."""
+  if sanitizer == 'asan':
+    return lambda returncode, stdout, stderr: _asan_to_comments(stdout)
+  if sanitizer == 'msan':
+    return lambda returncode, stdout, stderr: _msan_to_comments(stdout)
+  if sanitizer == 'tsan':
+    return lambda returncode, stdout, stderr: _tsan_to_comments(stdout)
+  if sanitizer == 'ubsan':
+    return lambda returncode, stdout, stderr: _ubsan_to_comments(stdout)
+  raise ValueError('Unknown sanitizer: [{}]'.format(sanitizer))
+
+
+def get_validator_for(sanitizer):
+  """Returns a function that validates output for |sanitizer|.
+
+  The returned validator takes: returncode, stdout, stderr and returns
+  True if the output should be considered a success of False if the output
+  indicates an error of some kind occured.
+
+  Args:
+    sanitizer: 'asan', 'msan', 'tsan', or 'ubsan'
+
+  Returns:
+    Validator function.
+
+  Raises:
+    ValueError: for unknown |sanitizer|.
+  """
+  if sanitizer == 'asan':
+    return _asan_lsan_validator
+  if sanitizer == 'msan':
+    return _msan_validator
+  if sanitizer == 'tsan':
+    return _tsan_validator
+  if sanitizer == 'ubsan':
+    return _ubsan_validator
+  raise ValueError('Unknown sanitizer: [{}]'.format(sanitizer))
+
+
+def _asan_lsan_validator(returncode, stdout, stderr):
+  del stderr  # unused
+  asan_error_msg = 'ERROR: {}'.format(_SANITIZER_STRING['asan'])
+  lsan_error_msg = 'ERROR: {}'.format(_SANITIZER_STRING['lsan'])
+  asan_summary_msg = 'SUMMARY: {}'.format(_SANITIZER_STRING['asan'])
+  error_indicator = ((asan_error_msg in stdout or lsan_error_msg in stdout) and
+                     (asan_summary_msg in stdout))
+  return returncode == 0 or not error_indicator
+
+
+def _msan_validator(returncode, stdout, stderr):
+  del stderr  # unused
+  warning_msg = 'WARNING: {}'.format(_SANITIZER_STRING['msan'])
+  summary_msg = 'SUMMARY: {}'.format(_SANITIZER_STRING['msan'])
+  error_indicator = (warning_msg in stdout) and (summary_msg in stdout)
+  return returncode == 0 or not error_indicator
+
+
+def _tsan_validator(returncode, stdout, stderr):
+  del stderr  # unused
+  warning_msg = 'WARNING: {}'.format(_SANITIZER_STRING['tsan'])
+  summary_msg = 'SUMMARY: {}'.format(_SANITIZER_STRING['tsan'])
+  error_indicator = (warning_msg in stdout) and (summary_msg in stdout)
+  return returncode == 0 or not error_indicator
+
+
+def _ubsan_validator(returncode, stdout, stderr):
+  del stderr  # unused
+  return returncode == 0 or _SANITIZER_STRING['ubsan'] not in stdout
+
+
+def _asan_to_comments(sanitizer_output, lsan=True):
+  """Convert the output from ASAN into comments for Gerrit.
+
+  Args:
+    sanitizer_output: Output from running a binary with ASAN enabled.
+    lsan: Set True if asan/lsan were run together, to get comments from both.
+        (Default is True)
+
+  Returns:
+    A review dict suitable for passing to base_step.add_review
+  """
+  sanitizer_strings = [_SANITIZER_STRING['asan']]
+  if lsan:
+    sanitizer_strings.append(_SANITIZER_STRING['lsan'])
+
+  return _sanitizer_to_comments(sanitizer_strings, sanitizer_output)
+
+
+def _msan_to_comments(sanitizer_output):
+  return _sanitizer_to_comments([_SANITIZER_STRING['msan']], sanitizer_output)
+
+
+def _tsan_to_comments(sanitizer_output):
+  return _sanitizer_to_comments([_SANITIZER_STRING['tsan']], sanitizer_output)
+
+
+def _ubsan_to_comments(sanitizer_output):
+  """Convert output from UBSAN into comments for Gerrit.
+
+  UBSAN output is much different than other sanitizers, so it needs to
+  be handled specially.
+
+  Args:
+    sanitizer_output: Output from running a binary with ubsan enabled.
+
+  Returns:
+    A review dict suitable for passing to base_step.add_review
+  """
+  all_errors = []
+  for line in sanitizer_output.splitlines(True):
+    if _SANITIZER_STRING['ubsan'] in line:
+      all_errors.append(line)
+      if len(all_errors) > _MAX_ERRORS:
+        break
+
+  if len(all_errors) > _MAX_ERRORS:
+    all_errors = all_errors[:_MAX_ERRORS]
+    all_errors.append('<Too many errors to print. Check logs for more.>')
+
+  if all_errors:
+    return {'review': {'message': '\n\n'.join(all_errors)}}
+  return {}
+
+
+def _sanitizer_to_comments(sanitizer_strings, sanitizer_output):
+  """Converts output from ASAN/LSAN/MSAN/TSAN into comments for Gerrit.
+
+  Aside from UBSAN, all the sanitizers generate output that is in roughly
+  the same format, so they can all get parsed/scraped the same way.
+
+  Args:
+    sanitizer_strings: List of the identifying strings to search for in the logs
+      (e.g. AddressSanitizer, LeakSanitizer, MemorySanitizer, ThreadSanitizer)
+    sanitizer_output: Output from running a binary with a sanitizer enabled.
+
+  Returns:
+    A review dict suitable for passing to base_step.add_review
+  """
+  all_errors = []
+  current_error = []
+  for line in sanitizer_output.splitlines(True):
+    if _is_start_of_error(line, sanitizer_strings):
+      current_error = [line]
+    elif current_error and _is_end_of_error(line, sanitizer_strings):
+      current_error.append(line)
+      all_errors.append(''.join(_shorten_error_if_necessary(current_error)))
+      current_error = []
+      if len(all_errors) > _MAX_ERRORS:
+        break
+    elif current_error:
+      current_error.append(line)
+
+  if len(all_errors) > _MAX_ERRORS:
+    all_errors = all_errors[:_MAX_ERRORS]
+    all_errors.append(_TOO_MANY_ERRORS_MSG)
+
+  if all_errors:
+    return {'review': {'message': '\n\n'.join(all_errors)}}
+  return {}
+
+
+def _is_start_of_error(line, sanitizer_strings):
+  warning_patterns = ['WARNING: {}'.format(s) for s in sanitizer_strings]
+  error_patterns = ['ERROR: {}'.format(s) for s in sanitizer_strings]
+  begin_error_patterns = warning_patterns + error_patterns
+  return any(begin_error in line for begin_error in begin_error_patterns)
+
+
+def _is_end_of_error(line, sanitizer_strings):
+  end_error_patterns = ['SUMMARY: {}'.format(s) for s in sanitizer_strings]
+  return any(end_error in line for end_error in end_error_patterns)
+
+
+def _shorten_error_if_necessary(error):
+  if len(error) <= _MAX_LINES_PER_ERROR:
+    return error
+
+  start_of_error = error[:int(_MAX_LINES_PER_ERROR / 2)]
+  end_of_error = error[int(-1 * _MAX_LINES_PER_ERROR / 2):]
+  num_snipped_lines = len(error) - _MAX_LINES_PER_ERROR
+  snipped_msg = [_ERROR_SNIPPED_MSG.format(num_snipped_lines)]
+  return start_of_error + snipped_msg + end_of_error
diff --git a/cq/scripts/helpers/sanitizer_utils_test.py b/cq/scripts/helpers/sanitizer_utils_test.py
new file mode 100644
index 0000000..7813e8b
--- /dev/null
+++ b/cq/scripts/helpers/sanitizer_utils_test.py
@@ -0,0 +1,115 @@
+"""Tests for sanitizer_utils."""
+
+from __future__ import absolute_import
+import os
+import sys
+import unittest
+
+# Allow imports from cq/scripts folder
+sys.path.insert(
+    0, os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir)))
+from helpers import sanitizer_utils
+from helpers import sanitizer_utils_test_data as test_data
+
+
+class SanitizerUtilsTest(unittest.TestCase):
+
+  def testAsanToComments(self):
+    """Tests converting ASAN errors to Gerrit comments."""
+    comment_generator = sanitizer_utils.get_sanitizer_to_comments_for('asan')
+    actual_review = comment_generator(0, test_data.ASAN_ERROR_MESSAGE, '')
+    expected_review = {'review': {'message': test_data.ASAN_COMMENT}}
+    self.assertDictEqual(expected_review, actual_review)
+
+  def testAsanLsanToComments(self):
+    """Tests converting ASAN/LSAN combined errors to Gerrit comments."""
+    comment_generator = sanitizer_utils.get_sanitizer_to_comments_for('asan')
+    actual_review = comment_generator(0, test_data.ASAN_LSAN_ERROR_MESSAGE, '')
+    expected_review = {'review': {'message': test_data.ASAN_LSAN_COMMENT}}
+    self.assertDictEqual(expected_review, actual_review)
+
+  def testAsanValidator(self):
+    """Tests that ASAN errors are correctly detected from ASAN output."""
+    validator = sanitizer_utils.get_validator_for('asan')
+    self.assertTrue(validator(0, '', ''))
+    self.assertTrue(validator(0, 'Some stdout stuff', 'Some stderr'))
+    self.assertFalse(validator(1, test_data.ASAN_ERROR_MESSAGE, ''))
+
+  def testAsanLsanValidator(self):
+    """Tests that LSAN errors are correctly detected from ASAN/LSAN output."""
+    validator = sanitizer_utils.get_validator_for('asan')
+    self.assertFalse(validator(1, test_data.ASAN_LSAN_ERROR_MESSAGE, ''))
+
+  def testMsanToComments(self):
+    """Tests converting MSAN errors to Gerrit comments."""
+    comment_generator = sanitizer_utils.get_sanitizer_to_comments_for('msan')
+    actual_review = comment_generator(0, test_data.MSAN_ERROR_MESSAGE, '')
+    expected_review = {'review': {'message': test_data.MSAN_COMMENT}}
+    self.assertDictEqual(expected_review, actual_review)
+
+  def testMsanValidator(self):
+    """Tests that MSAN errors are correctly detected from MSAN output."""
+    validator = sanitizer_utils.get_validator_for('msan')
+    self.assertTrue(validator(0, '', ''))
+    self.assertTrue(validator(0, 'Some stdout stuff', 'Some stderr'))
+    self.assertFalse(validator(1, test_data.MSAN_ERROR_MESSAGE, ''))
+
+  def testTsanToComments(self):
+    """Tests converting TSAN errors to Gerrit comments."""
+    comment_generator = sanitizer_utils.get_sanitizer_to_comments_for('tsan')
+    actual_review = comment_generator(0, test_data.TSAN_ERROR_MESSAGE, '')
+    expected_review = {'review': {'message': test_data.TSAN_COMMENT}}
+    self.maxDiff = None
+    self.assertDictEqual(expected_review, actual_review)
+
+  def testTsanValidator(self):
+    """Tests that TSAN errors are correctly detected from TSAN output."""
+    validator = sanitizer_utils.get_validator_for('tsan')
+    self.assertTrue(validator(0, '', ''))
+    self.assertTrue(validator(0, 'Some stdout stuff', 'Some stderr'))
+    self.assertFalse(validator(1, test_data.TSAN_ERROR_MESSAGE, ''))
+
+  def testUbsanToComments(self):
+    """Tests converting UBSAN errors to Gerrit comments."""
+    comment_generator = sanitizer_utils.get_sanitizer_to_comments_for('ubsan')
+    actual_review = comment_generator(0, test_data.UBSAN_ERROR_MESSAGE, '')
+    self.maxDiff = None
+    expected_review = {
+        'review': {
+            'message':
+                '\n\n'.join([
+                    test_data.UBSAN_COMMENT,
+                    '<Too many errors to print. Check logs for more.>'
+                ])
+        }
+    }
+    self.assertDictEqual(expected_review, actual_review)
+
+  def testUbsanValidator(self):
+    """Tests that UBSAN errors are correctly detected from UBSAN output."""
+    validator = sanitizer_utils.get_validator_for('ubsan')
+    self.assertTrue(validator(0, '', ''))
+    self.assertTrue(validator(0, 'Some stdout stuff', 'Some stderr'))
+    self.assertFalse(validator(1, test_data.UBSAN_ERROR_MESSAGE, ''))
+
+  def testTruncateVeryLongErrorMessage(self):
+    """Tests that very long error messages get truncated."""
+    very_long_comment_builder = ['WARNING: AddressSanitizer']
+    very_long_comment_builder += ['line with error info'] * 1000
+    very_long_comment_builder += ['SUMMARY: AddressSanitizer']
+    very_long_comment = '\n'.join(very_long_comment_builder)
+
+    comment_generator = sanitizer_utils.get_sanitizer_to_comments_for('asan')
+    actual_review = comment_generator(0, very_long_comment, '')
+    actual_review_message = actual_review['review']['message']
+
+    self.assertIn('<...snip (error too long; removed 972 lines)...>',
+                  actual_review_message)
+
+    expected_message_length = sanitizer_utils._MAX_LINES_PER_ERROR + 1
+    actual_message_length = len(actual_review_message.splitlines())
+    self.assertEqual(actual_message_length, expected_message_length)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/sanitizer_utils_test_data.py b/cq/scripts/helpers/sanitizer_utils_test_data.py
new file mode 100644
index 0000000..4971845
--- /dev/null
+++ b/cq/scripts/helpers/sanitizer_utils_test_data.py
@@ -0,0 +1,230 @@
+"""Test data for sanitizer_utils_test.
+
+These are copy/pasted directly from the sanitizer logs from catabuilder. They
+live in a separate file because they are large and would otherwise clutter
+sanitizer_utils_test.py unnecessarily.
+"""
+# pylint: disable=line-too-long
+
+ASAN_ERROR_MESSAGE = (
+    """GMOCK WARNING:
+Uninteresting mock function call - returning directly.
+    Function call: UnregisterService(@0x618000000558 0x604000013f90, @0x7f9bbba11760 8-byte object <10-E2 01-00 40-60 00-00>)
+NOTE: You can safely ignore the above warning unless this call should not happen.
+GMOCK WARNING:
+Uninteresting mock function call - returning directly.
+    Function call: RemoveExternalNamespaceHandler
+=================================================================
+==23973==ERROR: AddressSanitizer: alloc-dealloc-mismatch (operator new [] vs operator delete) on 0x619000009180
+    #0 0x51a1fb in operator delete(void*) (out_clang_chromecast_gn_asan/Release/cast_multizone_unittests+0x51a1fb)
+    #1 0x5e2eca in operator() buildtools/third_party/libc++/trunk/include/memory:2529:13
+    #2 0x5e2eca in reset buildtools/third_party/libc++/trunk/include/memory:2735
+    #3 0x5e2eca in ~unique_ptr buildtools/third_party/libc++/trunk/include/memory:2703
+    #4 0x5e2eca in ~SocketBuffer chromecast/internal/multizone/fake_multizone_device.cc:39
+    #5 0x5e2eca in operator() buildtools/third_party/libc++/trunk/include/memory:2529
+
+0x619000009180 is located 0 bytes inside of 1024-byte region [0x619000009180,0x619000009580)
+allocated by thread T0 here:
+    #0 0x51973b in operator new[](unsigned long) (out_clang_chromecast_gn_asan/Release/cast_multizone_unittests+0x51973b)
+    #1 0x5a2173 in SocketBuffer chromecast/internal/multizone/fake_multizone_device.cc:34:15
+    #2 0x5a2173 in chromecast::multizone::FakeMultizoneDevice::StreamSocketPair::StreamSocketPair() chromecast/internal/multizone/fake_multizone_device.cc:203
+    #3 0x5a1cbd in chromecast::multizone::FakeMultizoneDevice::FakeMultizoneDevice
+    #4 0x64446f in chromecast::multizone::MultizoneManagerTest::AddFollower
+
+
+SUMMARY: AddressSanitizer: alloc-dealloc-mismatch (out_clang_chromecast_gn_asan/Release/cast_multizone_unittests+0x51a1fb) in operator delete(void*)
+==23973==HINT: if you don't care about these errors you may set ASAN_OPTIONS=alloc_dealloc_mismatch=0
+==23973==ABORTING
+[73/206] MultizoneManagerTest.LaunchAndUnlaunchLeader (CRASHED)
+[74/206] MultizoneMediaManagerTest.GetActiveState (2 ms)""")
+
+ASAN_COMMENT = (
+    """==23973==ERROR: AddressSanitizer: alloc-dealloc-mismatch (operator new [] vs operator delete) on 0x619000009180
+    #0 0x51a1fb in operator delete(void*) (out_clang_chromecast_gn_asan/Release/cast_multizone_unittests+0x51a1fb)
+    #1 0x5e2eca in operator() buildtools/third_party/libc++/trunk/include/memory:2529:13
+    #2 0x5e2eca in reset buildtools/third_party/libc++/trunk/include/memory:2735
+    #3 0x5e2eca in ~unique_ptr buildtools/third_party/libc++/trunk/include/memory:2703
+    #4 0x5e2eca in ~SocketBuffer chromecast/internal/multizone/fake_multizone_device.cc:39
+    #5 0x5e2eca in operator() buildtools/third_party/libc++/trunk/include/memory:2529
+
+0x619000009180 is located 0 bytes inside of 1024-byte region [0x619000009180,0x619000009580)
+allocated by thread T0 here:
+    #0 0x51973b in operator new[](unsigned long) (out_clang_chromecast_gn_asan/Release/cast_multizone_unittests+0x51973b)
+    #1 0x5a2173 in SocketBuffer chromecast/internal/multizone/fake_multizone_device.cc:34:15
+    #2 0x5a2173 in chromecast::multizone::FakeMultizoneDevice::StreamSocketPair::StreamSocketPair() chromecast/internal/multizone/fake_multizone_device.cc:203
+    #3 0x5a1cbd in chromecast::multizone::FakeMultizoneDevice::FakeMultizoneDevice
+    #4 0x64446f in chromecast::multizone::MultizoneManagerTest::AddFollower
+
+
+SUMMARY: AddressSanitizer: alloc-dealloc-mismatch (out_clang_chromecast_gn_asan/Release/cast_multizone_unittests+0x51a1fb) in operator delete(void*)
+""")
+
+ASAN_LSAN_ERROR_MESSAGE = (
+    """[----------] Global test environment tear-down
+[==========] 10 tests from 2 test cases ran. (24 ms total)
+[  PASSED  ] 10 tests.
+
+=================================================================
+==23625==ERROR: LeakSanitizer: detected memory leaks
+
+Direct leak of 320 byte(s) in 2 object(s) allocated from:
+    #0 0x4f616b in operator new(unsigned long) (/usr/local/google/home/mosaic-role/slave/repo_clients/internal/chromium/src/out_clang_chromecast_gn_asan/Release/cast_crash_unittests+0x4f616b)
+    #1 0x54b2ab in chromecast::CastCrashdumpUploader::CastCrashdumpUploader(chromecast::CastCrashdumpData const&) chromecast/crash/cast_crashdump_uploader.cc:37:35
+    #2 0x521291 in MockCastCrashdumpUploader chromecast/crash/linux/minidump_uploader_unittest.cc:54:9
+    #3 0x521291 in StrictMock<chromecast::CastCrashdumpData> testing/gmock/include/gmock/gmock-generated-nice-strict.h:284
+
+
+SUMMARY: AddressSanitizer: 320 byte(s) leaked in 2 allocation(s).
+[11/46] DumpInfoTest.TooFewFieldsIsNotValid (0 ms)
+[ RUN      ] DumpInfoTest.BadTimeStringIsNotValid
+[23625:23625:1109/162235:610901543961:INFO:dump_info.cc(147)] Failed to convert dump time invalid
+[       OK ] DumpInfoTest.BadTimeStringIsNotValid (1 ms)
+[ RUN      ] DumpInfoTest.AllRequiredFieldsIsValid
+[       OK ] DumpInfoTest.AllRequiredFieldsIsValid (0 ms)""")
+
+ASAN_LSAN_COMMENT = (
+    """==23625==ERROR: LeakSanitizer: detected memory leaks
+
+Direct leak of 320 byte(s) in 2 object(s) allocated from:
+    #0 0x4f616b in operator new(unsigned long) (/usr/local/google/home/mosaic-role/slave/repo_clients/internal/chromium/src/out_clang_chromecast_gn_asan/Release/cast_crash_unittests+0x4f616b)
+    #1 0x54b2ab in chromecast::CastCrashdumpUploader::CastCrashdumpUploader(chromecast::CastCrashdumpData const&) chromecast/crash/cast_crashdump_uploader.cc:37:35
+    #2 0x521291 in MockCastCrashdumpUploader chromecast/crash/linux/minidump_uploader_unittest.cc:54:9
+    #3 0x521291 in StrictMock<chromecast::CastCrashdumpData> testing/gmock/include/gmock/gmock-generated-nice-strict.h:284
+
+
+SUMMARY: AddressSanitizer: 320 byte(s) leaked in 2 allocation(s).
+""")
+
+MSAN_ERROR_MESSAGE = (
+    """[85/97] HotspotManagerTest.Start_Mutliple_Stop_Multiple_Mixed (7 ms)
+[86/97] UmaHashUtilTest.HashBuildNumber64 (1 ms)
+[87/97] UmaHashUtilTest.HashSdkVersion64 (0 ms)
+[1109/162418:ERROR:kill_posix.cc(84)] Unable to terminate process group 9794: No such process
+[ RUN      ] UdpSocketWrapperTest.DeleteInCallback
+Uninitialized bytes in __interceptor_sendto at offset 0 inside [0x7010000007c0, 1)
+==9794==WARNING: MemorySanitizer: use-of-uninitialized-value
+    #0 0x928115 in net::UDPSocketPosix::InternalSendTo(net::IOBuffer*, int, net::IPEndPoint const*) net/udp/udp_socket_posix.cc:636:16
+    #1 0x92742d in net::UDPSocketPosix::SendToOrWrite(net::IOBuffer*, int, net::IPEndPoint const*, base::Callback<void (int), (base::internal::CopyMode)1, (base::internal::RepeatMode)1> const&) net/udp/udp_socket_posix.cc:254:16
+    #2 0x65a4d7 in chromecast::UdpSocketWrapper::SendTo(scoped_refptr<net::IOBuffer> const&, int, net::IPEndPoint const&) chromecast/internal/net/udp_socket_wrapper.cc:113:23
+
+  Uninitialized value was created by a heap allocation
+    #0 0x4a6662 in operator new[](unsigned long) (/usr/local/google/home/mosaic-role/slave/repo_clients/internal/chromium/src/out_clang_chromecast_gn_msan/Release/cast_net_internal_unittests+0x4a6662)
+    #1 0x8bcdc3 in net::IOBuffer::IOBuffer(int) net/base/io_buffer.cc:35:11
+    #2 0x8bd468 in net::IOBufferWithSize::IOBufferWithSize(int) net/base/io_buffer.cc:53:7
+
+SUMMARY: MemorySanitizer: use-of-uninitialized-value net/udp/udp_socket_posix.cc:636:16 in net::UDPSocketPosix::InternalSendTo(net::IOBuffer*, int, net::IPEndPoint const*)
+Exiting
+[88/97] UdpSocketWrapperTest.DeleteInCallback (CRASHED)
+[89/97] UdpSocketWrapperTest.StartStopStart (3 ms)""")
+
+MSAN_COMMENT = (
+    """==9794==WARNING: MemorySanitizer: use-of-uninitialized-value
+    #0 0x928115 in net::UDPSocketPosix::InternalSendTo(net::IOBuffer*, int, net::IPEndPoint const*) net/udp/udp_socket_posix.cc:636:16
+    #1 0x92742d in net::UDPSocketPosix::SendToOrWrite(net::IOBuffer*, int, net::IPEndPoint const*, base::Callback<void (int), (base::internal::CopyMode)1, (base::internal::RepeatMode)1> const&) net/udp/udp_socket_posix.cc:254:16
+    #2 0x65a4d7 in chromecast::UdpSocketWrapper::SendTo(scoped_refptr<net::IOBuffer> const&, int, net::IPEndPoint const&) chromecast/internal/net/udp_socket_wrapper.cc:113:23
+
+  Uninitialized value was created by a heap allocation
+    #0 0x4a6662 in operator new[](unsigned long) (/usr/local/google/home/mosaic-role/slave/repo_clients/internal/chromium/src/out_clang_chromecast_gn_msan/Release/cast_net_internal_unittests+0x4a6662)
+    #1 0x8bcdc3 in net::IOBuffer::IOBuffer(int) net/base/io_buffer.cc:35:11
+    #2 0x8bd468 in net::IOBufferWithSize::IOBufferWithSize(int) net/base/io_buffer.cc:53:7
+
+SUMMARY: MemorySanitizer: use-of-uninitialized-value net/udp/udp_socket_posix.cc:636:16 in net::UDPSocketPosix::InternalSendTo(net::IOBuffer*, int, net::IPEndPoint const*)
+""")
+
+TSAN_ERROR_MESSAGE = (
+    """ThreadSanitizer: reported 1 warnings
+[814/4508] FileSystemOperationImplWriteTest.TestWriteInvalidFile (5 ms)
+[ RUN      ] FileSystemOperationImplWriteTest.TestWriteDir
+[       OK ] FileSystemOperationImplWriteTest.TestWriteDir (5 ms)
+
+[----------] 2 tests from FileSystemOperationRunnerTest
+[ RUN      ] FileSystemOperationRunnerTest.NotFoundError
+[19800:19800:1109/163405:702831287703:WARNING:sandbox_quota_observer.cc(114)] Could not get usage cache path for: filesystem:http://example.com/temporary/foo
+[19800:19800:1109/163405:702831288401:WARNING:obfuscated_file_util.cc(1181)] Failed to get origin+type directory: filesystem:http://example.com/temporary/foo error:-4
+[19800:19800:1109/163405:702831288884:WARNING:sandbox_quota_observer.cc(114)] Could not get usage cache path for: filesystem:http://example.com/temporary/foo
+[       OK ] FileSystemOperationRunnerTest.NotFoundError (10 ms)
+[----------] 2 tests from FileSystemOperationRunnerTest (14 ms total)
+
+[----------] Global test environment tear-down
+[==========] 10 tests from 2 test cases ran. (97 ms total)
+[  PASSED  ] 10 tests.
+==================
+WARNING: ThreadSanitizer: data race (pid=19800)
+  Read of size 4 at 0x0000097b6dc0 by thread T1:
+    #0 logging::ShouldCreateLogMessage(int) base/logging.cc:412:10 (content_unittests+0x000003aefaa7)
+    #1 operator-> base/memory/ref_counted.h:319:5 (content_unittests+0x000003afb7e3)
+    #2 base::MessageLoop::BindToCurrentThread() base/message_loop/message_loop.cc:343 (content_unittests+0x000003afb7e3)
+    #3 base::Thread::ThreadMain() base/threading/thread.cc:299:18 (content_unittests+0x000003b5d100)
+    #4 base::(anonymous namespace)::ThreadFunc(void*) base/threading/platform_thread_posix.cc:71:13 (content_unittests+0x000003b535d8)
+
+  Previous write of size 4 at 0x0000097b6dc0 by main thread:
+    #0 logging::BaseInitLoggingImpl_built_with_NDEBUG(logging::LoggingSettings const&) base/logging.cc:373:25 (content_unittests+0x000003aef754)
+    #1 InitLogging base/logging.h:232:10 (content_unittests+0x000002fc08c3)
+    #2 base::TestSuite::Initialize() base/test/test_suite.cc:331 (content_unittests+0x000002fc08c3)
+    #3 content::ContentTestSuiteBase::Initialize() content/public/test/content_test_suite_base.cc:61:20 (content_unittests+0x000002fb2ccf)
+
+  Location is global 'logging::(anonymous namespace)::g_logging_destination' of size 4 at 0x0000097b6dc0 (content_unittests+0x0000097b6dc0)
+
+  Thread T1 'test_io_thread' (tid=19902, running) created by main thread at:
+    #0 pthread_create <null> (content_unittests+0x00000050c9b5)
+    #1 base::(anonymous namespace)::CreateThread(unsigned long, bool, base::PlatformThread::Delegate*, base::PlatformThreadHandle*, base::ThreadPriority) base/threading/platform_thread_posix.cc:110:13 (content_unittests+0x000003b53175)
+    #2 base::PlatformThread::CreateWithPriority(unsigned long, base::PlatformThread::Delegate*, base::PlatformThreadHandle*, base::ThreadPriority) base/threading/platform_thread_posix.cc:191:10 (content_unittests+0x000003b53025)
+
+SUMMARY: ThreadSanitizer: data race base/logging.cc:412:10 in logging::ShouldCreateLogMessage(int)
+==================""")
+
+TSAN_COMMENT = (
+    """WARNING: ThreadSanitizer: data race (pid=19800)
+  Read of size 4 at 0x0000097b6dc0 by thread T1:
+    #0 logging::ShouldCreateLogMessage(int) base/logging.cc:412:10 (content_unittests+0x000003aefaa7)
+    #1 operator-> base/memory/ref_counted.h:319:5 (content_unittests+0x000003afb7e3)
+    #2 base::MessageLoop::BindToCurrentThread() base/message_loop/message_loop.cc:343 (content_unittests+0x000003afb7e3)
+    #3 base::Thread::ThreadMain() base/threading/thread.cc:299:18 (content_unittests+0x000003b5d100)
+    #4 base::(anonymous namespace)::ThreadFunc(void*) base/threading/platform_thread_posix.cc:71:13 (content_unittests+0x000003b535d8)
+
+  Previous write of size 4 at 0x0000097b6dc0 by main thread:
+    #0 logging::BaseInitLoggingImpl_built_with_NDEBUG(logging::LoggingSettings const&) base/logging.cc:373:25 (content_unittests+0x000003aef754)
+    #1 InitLogging base/logging.h:232:10 (content_unittests+0x000002fc08c3)
+    #2 base::TestSuite::Initialize() base/test/test_suite.cc:331 (content_unittests+0x000002fc08c3)
+    #3 content::ContentTestSuiteBase::Initialize() content/public/test/content_test_suite_base.cc:61:20 (content_unittests+0x000002fb2ccf)
+
+  Location is global 'logging::(anonymous namespace)::g_logging_destination' of size 4 at 0x0000097b6dc0 (content_unittests+0x0000097b6dc0)
+
+  Thread T1 'test_io_thread' (tid=19902, running) created by main thread at:
+    #0 pthread_create <null> (content_unittests+0x00000050c9b5)
+    #1 base::(anonymous namespace)::CreateThread(unsigned long, bool, base::PlatformThread::Delegate*, base::PlatformThreadHandle*, base::ThreadPriority) base/threading/platform_thread_posix.cc:110:13 (content_unittests+0x000003b53175)
+    #2 base::PlatformThread::CreateWithPriority(unsigned long, base::PlatformThread::Delegate*, base::PlatformThreadHandle*, base::ThreadPriority) base/threading/platform_thread_posix.cc:191:10 (content_unittests+0x000003b53025)
+
+SUMMARY: ThreadSanitizer: data race base/logging.cc:412:10 in logging::ShouldCreateLogMessage(int)
+""")
+
+UBSAN_ERROR_MESSAGE = (
+    """INFO:root:running command: chromium/src/out_clang_chromecast_gn_ubsan/Release/net_unittests --ozone-platform=headless --test-launcher-bot-mode  --gtest_filter=-:* --gtest_output=xml:/tmp/reports/test_detail_net_unittests.xml
+../../buildtools/third_party/libc++/trunk/include/list:208:16: runtime error: downcast of address 0x000006a388f8 with insufficient space for an object of type 'std::__1::__list_node<std::__1::pair<net::(anonymous namespace)::TransactionPhase, void (*)(net::MockHttpCache *)>, void *>'
+0x000006a388f8: note: pointer points here
+ 3c 12 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00
+    #0 0x47a615  (/usr/local/google/home/mosaic-role/slave/repo_clients/internal/chromium/src/out_clang_chromecast_gn_ubsan/Release/net_unittests+0x47a615)
+    #1 0x350073c  (/usr/local/google/home/mosaic-role/slave/repo_clients/internal/chromium/src/out_clang_chromecast_gn_ubsan/Release/net_unittests+0x350073c)
+    #2 0x7f1bab1fded4  (/lib/x86_64-linux-gnu/libc.so.6+0x21ed4)
+
+../../buildtools/third_party/libc++/trunk/include/list:560:31: runtime error: downcast of address 0x000006a388f8 with insufficient space for an object of type 'std::__1::__list_node<std::__1::pair<net::(anonymous namespace)::TransactionPhase, void (*)(net::MockHttpCache *)>, void *>'
+0x000006a388f8: note: pointer points here
+ 3c 12 00 00  60 ff b7 ae 3c 12 00 00  a0 fb b7 ae 3c 12 00 00  09 00 00 00 00 00 00 00  00 00 00 00
+    #0 0xb9929d  (/usr/local/google/home/mosaic-role/slave/repo_clients/internal/chromium/src/out_clang_chromecast_gn_ubsan/Release/net_unittests+0xb9929d)
+    #1 0xbb300c  (/usr/local/google/home/mosaic-role/slave/repo_clients/internal/chromium/src/out_clang_chromecast_gn_ubsan/Release/net_unittests+0xbb300c)
+    #2 0x2d71d51  (/usr/local/google/home/mosaic-role/slave/repo_clients/internal/chromium/src/out_clang_chromecast_gn_ubsan/Release/net_unittests+0x2d71d51)
+
+IMPORTANT DEBUGGING NOTE: batches of tests are run inside their
+own process. For debugging a test inside a debugger, use the
+--gtest_filter=<your_test_name> flag along with
+--single-process-tests.
+Using sharding settings from environment. This is shard 0/1
+Using 32 parallel jobs.
+0 tests run
+Tests took 0 seconds.
+INFO:root:completed running command: chromium/src/out_clang_chromecast_gn_ubsan/Release/net_unittests --ozone-platform=headless --test-launcher-bot-mode  --gtest_filter=-:* --gtest_output=xml:/tmp/reports/test_detail_net_unittests.xml""")
+
+
+UBSAN_COMMENT = (
+    """../../buildtools/third_party/libc++/trunk/include/list:208:16: runtime error: downcast of address 0x000006a388f8 with insufficient space for an object of type 'std::__1::__list_node<std::__1::pair<net::(anonymous namespace)::TransactionPhase, void (*)(net::MockHttpCache *)>, void *>'
+""")
diff --git a/cq/scripts/helpers/signal_utils.py b/cq/scripts/helpers/signal_utils.py
new file mode 100644
index 0000000..aed36b6
--- /dev/null
+++ b/cq/scripts/helpers/signal_utils.py
@@ -0,0 +1,26 @@
+"""Simple context managers for handling signals."""
+
+from __future__ import absolute_import
+import contextlib
+import signal
+
+
+class SigtermException(Exception):
+  """Exception for getting a SIGTERM."""
+
+
+def _sigterm_handler(signum, frame):
+  del signum, frame  # Unused.
+  raise SigtermException()
+
+
+@contextlib.contextmanager
+def sigterm_handler():
+  """A simple handler for SIGTERM."""
+
+  old_sigterm_handler = signal.signal(signal.SIGTERM, _sigterm_handler)
+  try:
+    yield
+  finally:
+    # Reset previous handlers
+    signal.signal(signal.SIGTERM, old_sigterm_handler)
diff --git a/cq/scripts/helpers/timeout.py b/cq/scripts/helpers/timeout.py
new file mode 100644
index 0000000..8ebeee4
--- /dev/null
+++ b/cq/scripts/helpers/timeout.py
@@ -0,0 +1,62 @@
+"""A simple timeout context manager.
+
+Note: this depends on SIGALRM and is thus UNIX specific;
+      this will not work on Windows.
+"""
+
+from __future__ import absolute_import
+import contextlib
+import signal
+
+
+class TimeoutException(Exception):
+  """Exception for timing out."""
+
+
+def _default_timeout_handler(signum, frame):
+  del signum, frame  # Unused.
+  raise TimeoutException()
+
+
+@contextlib.contextmanager
+def timeout(handler=_default_timeout_handler, timeout_secs=0):
+  """A simple timeout contextmanager based on SIGALRM.
+
+  Usage:
+
+  try:
+    with timeout(handler=my_handler, timeout_secs=30):
+      do_stuff_that_might_take_too_long()
+  except timeout.TimeoutException:
+    clean_up_from_timeout()
+  finally:
+    other_clean_up()
+
+  Note: Only one timeout instance can be used at a time, since the
+        signal handler is changed and all previously set SIGALARMs
+        are cleared.
+
+  Args:
+    handler: A function called with two arguments:
+               |signum| (the signal number which should be signal.SIGALRM)
+               |frame| (the current stack frame, which can be None)
+             (Defaults to a function that just raises TimeoutException.)
+    timeout_secs: The amount of time before a timeout, in seconds.
+
+  Yields:
+    Nothing. Yields is used as part of the contextmanager construct.
+
+  Raises:
+    timeout.TimeoutException (if the default handler is used).
+  """
+  old_handler = signal.signal(signal.SIGALRM, handler)
+  old_timeout = signal.alarm(timeout_secs)
+  try:
+    yield
+  except:
+    # Catch any exceptions (e.g. TimeoutException) that get raised so that
+    # the clean-up in the finally block can happen. Then re-raise the exception.
+    raise
+  finally:
+    signal.signal(signal.SIGALRM, old_handler)  # reset the previous handler
+    signal.alarm(old_timeout)  # reset the previous time remaining
diff --git a/cq/scripts/helpers/timeout_test.py b/cq/scripts/helpers/timeout_test.py
new file mode 100644
index 0000000..f218463
--- /dev/null
+++ b/cq/scripts/helpers/timeout_test.py
@@ -0,0 +1,102 @@
+"""Tests for timeout."""
+
+from __future__ import absolute_import
+import signal
+import time
+import unittest
+
+from . import timeout
+
+LONG_TIME_SECS = 10
+
+
+class TimeoutTest(unittest.TestCase):
+
+  def testRunWithoutTimeout(self):
+    """Tests that handler is not called if there is no timeout."""
+    def _Handler(signum, frame):
+      del signum, frame  # Unused.
+      self.fail('Timeout should not occur.')
+
+    with timeout.timeout(handler=_Handler, timeout_secs=LONG_TIME_SECS):
+      pass
+
+  def testRunWithZeroTimeout(self):
+    """Tests that SIGALRM isn't raised with a 0 second timeout."""
+    def _Handler(signum, frame):
+      del signum, frame  # Unused.
+      self.fail('Timeout should not occur.')
+
+    with timeout.timeout(handler=_Handler, timeout_secs=0):
+      pass
+
+  def testDefaultHandler(self):
+    """Tests the default handler raises the correct exception."""
+    with self.assertRaises(timeout.TimeoutException):
+      with timeout.timeout(timeout_secs=1):
+        time.sleep(LONG_TIME_SECS)
+
+  def testRunWithTimeout(self):
+    """Tests timing out with a custom timeout handler."""
+
+    class TestException(Exception):
+      """A custom exception for this test."""
+
+    def _Handler(signum, frame):
+      del frame  # Unused.
+      self.assertEqual(signum, signal.SIGALRM)
+      raise TestException()
+
+    with self.assertRaises(TestException):
+      with timeout.timeout(handler=_Handler, timeout_secs=1):
+        time.sleep(LONG_TIME_SECS)
+
+  def testPreviouslySetHandlerIsCalledWithoutTimeout(self):
+    """Tests that previous handler is called if there is no timeout."""
+
+    class TestException(Exception):
+      """A custom exception for this test."""
+
+    def _FirstHandler(signum, frame):
+      del frame  # Unused.
+      self.assertEqual(signum, signal.SIGALRM)
+      raise TestException()
+
+    def _SecondHandler(signum, frame):
+      del signum, frame  # Unused.
+      self.fail('Timeout should not occur.')
+
+    with self.assertRaises(TestException):
+      signal.signal(signal.SIGALRM, _FirstHandler)
+      signal.alarm(2)
+      with timeout.timeout(handler=_SecondHandler, timeout_secs=LONG_TIME_SECS):
+        pass
+      time.sleep(LONG_TIME_SECS)
+
+  def testPreviouslySetHandlerIsCalledWithTimeout(self):
+    """Tests that previous handler is called if there is a timeout."""
+
+    class TestException(Exception):
+      """A custom exception for this test."""
+
+    def _FirstHandler(signum, frame):
+      del frame  # Unused.
+      self.assertEqual(signum, signal.SIGALRM)
+      raise TestException()
+
+    def _SecondHandler(signum, frame):
+      del frame  # Unused.
+      self.assertEqual(signum, signal.SIGALRM)
+      raise TestException()
+
+    with self.assertRaises(TestException):
+      signal.signal(signal.SIGALRM, _FirstHandler)
+      signal.alarm(2)
+      with self.assertRaises(TestException):
+        with timeout.timeout(handler=_SecondHandler, timeout_secs=1):
+          time.sleep(LONG_TIME_SECS)
+      time.sleep(LONG_TIME_SECS)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/typo_fixer.py b/cq/scripts/helpers/typo_fixer.py
new file mode 100644
index 0000000..c706885
--- /dev/null
+++ b/cq/scripts/helpers/typo_fixer.py
@@ -0,0 +1,41 @@
+"""Tool for fixing for common typos."""
+
+from __future__ import absolute_import
+import collections
+import logging
+import os
+import re
+
+from helpers import typos
+
+
+def fix(line):
+  """Return a list of (typo, suggestion) for typos in |line|."""
+  suggestions = []
+  for typo in typos.TYPOS:
+    for match in re.finditer(typo.typo_pattern, line, re.IGNORECASE):
+      original = match.group(0)
+      suggestion = re.sub(
+          typo.typo_pattern,
+          typo.replacement_pattern,
+          original,
+          count=1,
+          flags=re.IGNORECASE)
+
+      # The typo check was triggering too many false positives when words
+      # were mixed with CamelCase. Detect that case and ignore those suggestions
+      # to reduce the false positive case.
+      if re.search(r'[a-z][A-Z]', original):
+        continue
+
+      if original.islower():
+        suggestion = suggestion.lower()
+      elif original.isupper():
+        suggestion = suggestion.upper()
+      elif original.istitle():
+        suggestion = suggestion.title()
+
+      if original != suggestion:
+        suggestions.append((original, suggestion))
+
+  return suggestions
diff --git a/cq/scripts/helpers/typo_fixer_test.py b/cq/scripts/helpers/typo_fixer_test.py
new file mode 100644
index 0000000..0ae468a
--- /dev/null
+++ b/cq/scripts/helpers/typo_fixer_test.py
@@ -0,0 +1,111 @@
+"""Tests for typo_fixer."""
+
+from __future__ import absolute_import
+import os
+import sys
+import unittest
+
+from unittest import mock
+
+# Allow imports from cq/scripts folder
+# pylint: disable=wrong-import-position
+sys.path.insert(0, os.path.realpath(os.path.join(
+    os.path.dirname(__file__), os.pardir)))
+from helpers import typo_fixer
+
+
+class AssertItemsEqualMixin():
+  def assertItemsEqual(self, first, second):
+    return self.assertEqual(sorted(first), sorted(second))
+
+# Behaviour tests for the typo fixer.
+class TypoFixerTest(unittest.TestCase, AssertItemsEqualMixin):
+
+  def testDefaultDictionary_NoCorrections(self):
+    self.assertEqual([], typo_fixer.fix('This has no spelling mistakes.'))
+
+  def testDefaultDictionary_SingleWord_SingleCorrection(self):
+    self.assertItemsEqual([('shoudl', 'should')], typo_fixer.fix('shoudl'))
+    self.assertItemsEqual([('taget', 'target')], typo_fixer.fix('taget'))
+    self.assertItemsEqual([('santizier', 'sanitizer')],
+                          typo_fixer.fix('santizier'))
+
+  def testDefaultDictionary_Sentence_SingleCorrection(self):
+    self.assertItemsEqual([('elasped', 'elapsed')],
+                          typo_fixer.fix('Time has elasped'))
+    self.assertItemsEqual([('propogates', 'propagates')],
+                          typo_fixer.fix('Foo propogates this.'))
+    self.assertItemsEqual([('Inhert', 'Inherit')],
+                          typo_fixer.fix('Inhert from parent'))
+
+  def testDefaultDictionary_Sentence_MultipleCorrections(self):
+    self.assertItemsEqual([('libary', 'library'),
+                      ('guaratee', 'guarantee'),
+                      ('postive', 'positive')],
+                     typo_fixer.fix('The libary guaratees this is postive'))
+
+  def testDefaultDictionary_PreserveCase(self):
+    self.assertItemsEqual([('LIBARY', 'LIBRARY')], typo_fixer.fix('LIBARY'))
+    self.assertItemsEqual([('Libary', 'Library')], typo_fixer.fix('Libary'))
+    self.assertItemsEqual([('libary', 'library')], typo_fixer.fix('libary'))
+
+
+# A colleciton of positive / negative examples for individual typos.
+class TyposTest(unittest.TestCase, AssertItemsEqualMixin):
+
+  def testThenThan(self):
+    # Positive
+    self.assertItemsEqual([('less then', 'less than')],
+                          typo_fixer.fix('less then three'))
+    self.assertItemsEqual([('greater   then', 'greater   than')],
+                          typo_fixer.fix('greater   then'))
+    # Negative
+    self.assertItemsEqual([], typo_fixer.fix('bigger than 3, smaller than 4'))
+
+  def testDependency(self):
+    # Positive
+    self.assertItemsEqual([('depedency', 'dependency')],
+                          typo_fixer.fix('depedency'))
+    self.assertItemsEqual([('depedencies', 'dependencies')],
+                          typo_fixer.fix('depedencies'))
+    # Negative
+    self.assertItemsEqual([], typo_fixer.fix('dependency dependencies'))
+
+  def testPerference(self):
+    # Positive
+    self.assertItemsEqual([('prefferrance', 'preference')],
+                          typo_fixer.fix('That is my prefferrance'))
+
+    # Negative
+    self.assertItemsEqual([], typo_fixer.fix('What are your preferences?'))
+
+  def testTodo(self):
+    # Positive
+    self.assertItemsEqual([('TOOO(abc)', 'TODO(abc)')],
+                          typo_fixer.fix('TOOO(abc): A thing'))
+    self.assertItemsEqual([('TOOD(xyz@)', 'TODO(xyz@)')],
+                          typo_fixer.fix('TOOD(xyz@)'))
+    self.assertItemsEqual([('TDO(b/1234)', 'TODO(b/1234)')],
+                          typo_fixer.fix('TDO(b/1234): That last thing'))
+    # Negative
+    self.assertItemsEqual([], typo_fixer.fix(' TODO: something\n'
+                                             ' TODO(abc@): something\n'
+                                             ' TODO(b/1234): something\n'))
+
+  def testDefault(self):
+    #Negative
+    self.assertItemsEqual([], typo_fixer.fix('Make |kDefaultDelayMs| longer'))
+
+  def testSuccess(self):
+    # Postive
+    self.assertItemsEqual([('Successs', 'Success')],
+                           typo_fixer.fix('Successs on the mission today.'))
+
+    # Negative
+    # Note: this particular example does not generate a fix even though
+    # it matches "SuccessS", because the match has mixed case.
+    self.assertItemsEqual([], typo_fixer.fix('kGenericSuccessStatus'))
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/cq/scripts/helpers/typos.py b/cq/scripts/helpers/typos.py
new file mode 100644
index 0000000..b852f39
--- /dev/null
+++ b/cq/scripts/helpers/typos.py
@@ -0,0 +1,348 @@
+# This was copied from google3/devtools/linter/common_typos/typos.txt on
+# 2017-11-03 and modified to be compatible with the python re module.
+
+# NOTE: Typos here will apply to documentation, code, comments, and commit
+# messages. Please make sure that you do not add any typos here that exist in
+# projects' external dependencies (e.g. as part of a different project's API).
+#
+# Regular expressions on the left are replaced with those on the right.
+#
+# If you update this with anything complex, please add a corresponding test or
+# two for both positive and negative examples.
+#
+# Mostly taken and/or adapted from http://sites/lolcodesearch.
+#
+# Be careful with optional capture groups and references - optional groups
+# must be transformed as "(abc)?" -> "(abc|)", otherwise it may throw an
+# exception if the optional group isn't matched but is referenced in expansion.
+#
+# Only RE2 syntax regular expressions are supported.  Backreferences and
+# negative assertions make the expression non-regular, and take worst-case
+# exponential time.
+#
+# The typo fixer will ignore any suggestsions in which the original and the
+# suggested fix are the same, which can be useful since it is sometimes
+# to make a simple regex that matches all the misspellings while not matching
+# the correct spelling.
+
+# pylint: disable=line-too-long
+
+from __future__ import absolute_import
+import collections
+Typo = collections.namedtuple('Type', ['typo_pattern', 'replacement_pattern'])
+
+TYPOS = [
+    Typo(r'(a|)syncho(r|ro|)(r|n)ous(ly|)', r'\1synchronous\4'),
+    Typo(r'(con|de)(stuct|truct|cruct)', r'\1struct'),
+    Typo(r'(c|sh|w)oudl', r'\1ould'),
+    Typo(r'(c|sh|w)oul(e|n|s|t)', r'\1ould'),
+    Typo(r'(depricate|depreciate|deperecate)(d|)', r'deprecate\2'),
+    Typo(r'(m|re|)maping', r'\1mapping'),
+    Typo(r'(not|mod)(if|fi)cation(s|)', r'\1ification\3'),
+    Typo(r'(not|mod)ifica(iton|toin)(s|)', r'\1ification\3'),
+    Typo(r'(pre|)sumbit', r'\1submit'),
+    Typo(r'(proto|)tpye(s|)', r'\1type\2'),
+    Typo(r'(un|)healhty', r'\1healthy'),
+    Typo(r'(re|)genrate(ed|)', r'\1generate\2'),
+    Typo(r'\b(flase|fales|fasle)\b', r'false'),
+    Typo(r'\b(more|less|greater|lesser|bigger|smaller)(\s+)then\b',
+         r'\1\2than'),
+    Typo(r'\b(ture|treu)\b', r'true'),
+    Typo(r'\b(uknown|unkown|unkwon|unkowon)\b', r'unknown'),
+    Typo(r'\b(un|)singed\b', r'\1signed'),
+    Typo(r'\balot\b', r'a\x20lot'),
+    Typo(r'\bandoid\b', r'android'),
+    Typo(r'\bbalue\b', r'value'),
+    Typo(r'\bcorn(\s|)job', r'cron\1job'),
+    Typo(r'\bfrist\b', r'first'),
+    Typo(r'\bgrammer\b', r'grammar'),
+    Typo(r'\bit\sself(\.|)([^-a-zA-Z\.]|$)', r'itself\1\2'),
+    Typo(r'\bommitted', r'omitted'),
+    Typo(r'\bsmapl(e[rsd]?|ing)\b', r'sampl\1'),
+    Typo(r'\btaget\b', r'target'),
+    Typo(r'\bthier\b', r'their'),
+    Typo(r'\btread(.|)safe\b', r'thread\1safe'),
+    Typo(r'acccount', r'account'),
+    Typo(r'accessibile', r'accessible'),
+    Typo(r'accomodat(e|ion)', r'accommodat\1'),
+    Typo(r'accross', r'across'),
+    Typo(r'acitiv(e|ity)', r'activ\1'),
+    Typo(r'activites', r'activities'),
+    Typo(r'acutally', r'actually'),
+    Typo(r'\badition', r'addition'),
+    Typo(r'adress', r'address'),
+    Typo(r'aggragat(e|ed|ing|ion)', r'aggregat\1'),
+    Typo(r'aggree', r'agree'),
+    Typo(r'agressive', r'aggressive'),
+    Typo(r'ajust', r'adjust'),
+    Typo(r'alredy', r'already'),
+    Typo(r'andriod', r'android'),
+    Typo(r'applicaiton(s|)', r'application\1'),
+    Typo(r'argu(e|)me?nt(s|)', r'argument\2'),
+    Typo(r'arround', r'around'),
+    Typo(r'asign', r'assign'),
+    Typo(r'assosciated', r'associated'),
+    Typo(r'asume', r'assume'),
+    Typo(r'atribute', r'attribute'),
+    Typo(r'authentification', r'authentication'),
+    Typo(r'availiability', r'availability'),
+    Typo(r'availiable|avaliable', r'available'),
+    Typo(r'bandwith', r'bandwidth'),
+    Typo(r'baord', r'board'),
+    Typo(r'bec(u|)ase\b', r'because'),
+    Typo(r'befor\b', r'before'),
+    Typo(r'begining', r'beginning'),
+    Typo(r'borgcorn', r'borgcron'),
+    Typo(r'bormgon', r'borgmon'),
+    Typo(r'breif', r'brief'),
+    Typo(r'bulid(er|s|ing|)', r'build\1'),
+    Typo(r'calender', r'calendar'),
+    Typo(r'capbilit(y|ies)', r'capabilit\1'),
+    Typo(r'cannonical', r'canonical'),
+    Typo(r'celcius', r'Celsius'),
+    Typo(r'chagne', r'change'),
+    Typo(r'chanel', r'channel'),
+    Typo(r'charachter(s|)', r'character\1'),
+    Typo(r'choosen\b', r'chosen'),
+    Typo(r'choses\b', r'chooses'),
+    Typo(r'chrome(cats|csat)', r'chromecast'),
+    Typo(r'cilent', r'client'),
+    Typo(r'classif(er|cation)', r'classifi\1'),
+    Typo(r'columio', r'columnio'),
+    Typo(r'com+it+(ed|ing)\b', r'committ\1'),
+    Typo(r'comming\b', r'coming'),
+    Typo(r'comparision', r'comparison'),
+    Typo(r'compatability', r'compatibility'),
+    Typo(r'complier', r'compiler'),
+    Typo(r'(pre|)comptuation(s|)', r'\1computation\2'),
+    Typo(r'consistant', r'consistent'),
+    Typo(r'contaning', r'containing'),
+    Typo(r'contant(s|)', r'constant\1'),
+    Typo(r'contians', r'contains'),
+    Typo(r'continous', r'continuous'),
+    Typo(r'convertable', r'convertible'),
+    Typo(r'convertion', r'conversion'),
+    Typo(r'correspondan(ce|t)', r'corresponden\1'),
+    Typo(r'cosnt', r'const'),
+    Typo(r'course(.|)grained', r'coarse\1grained'),
+    Typo(r'curent\b', r'current'),
+    Typo(r'currenty', r'currently'),
+    Typo(r'dashborad', r'dashboard'),
+    Typo(r'deatil(s|ed|)', r'detail\1'),
+    Typo(r'deafult(s|)', r'default\1'),
+    Typo(r'definately', r'definitely'),
+    Typo(r'defintion(s|)', r'definition\1'),
+    Typo(r'dep(d|)e(n|)de(n|)c(y|ies)', r'dependenc\4'),
+    Typo(r'descritp(ions?|ors?)', r'descript\1'),
+    Typo(r'destript(ions?|ors?)', r'descript\1'),
+    Typo(r'deterine(s|d|)', r'determine\1'),
+    Typo(r'diferent', r'different'),
+    Typo(r'dimention', r'dimension'),
+    Typo(r'direcory', r'directory'),
+    Typo(r'dispatcer', r'dispatcher'),
+    Typo(r'egde(s|)', r'edge\1'),
+    Typo(r'elasped', r'elapsed'),
+    Typo(r'embeded', r'embedded'),
+    Typo(r'emmitt(ed|ing|er)', r'emitt\1'),
+    Typo(r'encryted', r'encrypted'),
+    Typo(r'encyrption', r'encryption'),
+    Typo(r'enity', r'entity'),
+    Typo(r'entires', r'entries\1'),
+    Typo(r'enviroment', r'environment'),
+    Typo(r'errror(s|)', r'error\1'),
+    Typo(r'estiamt(or|ed|e)', r'estimat\1'),
+    Typo(r'excecute', r'execute'),
+    Typo(r'excede', r'exceed'),
+    Typo(r'excelent', r'excellent'),
+    Typo(r'excercise', r'exercise'),
+    Typo(r'exisiting', r'existing'),
+    Typo(r'existan(t|ce)\b', r'existen\1'),
+    Typo(r'exmaple', r'example'),
+    Typo(r'expe(rie|ir|iri)ment(s|)', r'experiment\2'),
+    Typo(r'explicitely', r'explicitly'),
+    Typo(r'extenstion', r'extension'),
+    Typo(r'extention', r'extension'),
+    Typo(r'f(in|ni)shed', r'finished'),
+    Typo(r'farenheit', r'Fahrenheit'),
+    Typo(r'feas(i|a)b(i|)(le|ility)', r'feasib\3'),
+    Typo(r'fitler', r'filter'),
+    Typo(r'format(tt|)(ing|ed)', r'formatt\2'),
+    Typo(r'foward', r'forward'),
+    Typo(r'frequecy', r'frequency'),
+    Typo(r'func(iton|toin)(s|al|ed|ing|)', r'function\2'),
+    Typo(r'funtion', r'function'),
+    Typo(r'goole|googel|goolge', r'google'),
+    Typo(r'guaratee(d|)', r'guarantee\1'),
+    Typo(r'gurantee(d|)', r'guarantee\1'),
+    Typo(r'hanlder(s|)', r'handler\1'),
+    Typo(r'heigth', r'height'),
+    Typo(r'heirarchy', r'hierarchy'),
+    Typo(r'identifer(s|)', r'identifier\1'),
+    Typo(r'idenfifier(s|)', r'identifier\1'),
+    Typo(r'ignroe(d|s|)', r'ignore\1'),
+    Typo(r'impresssion', r'impression'),
+    Typo(r'ind(e|i)cies', r'indices'),
+    Typo(r'indeces', r'indices'),
+    Typo(r'indentif(ication|ied|ies|ier|iers|y|ying)', r'identif\1'),
+    Typo(r'independant', r'independent'),
+    Typo(r'ingored', r'ignored'),
+    Typo(r'inhert', r'inherit'),
+    Typo(r'initialzing', r'initializing'),
+    Typo(r'initialzi(ed|er|ers|es|ation)', r'initializ\1'),
+    Typo(r'instanciat(ed|ion|ions|e)', r'instantiat\1'),
+    Typo(r'instantitation', r'instantiation'),
+    Typo(r'intead', r'instead'),
+    Typo(r'interable', r'iterable'),
+    Typo(r'interator', r'iterator'),
+    Typo(r'interchangabl(e|y)', r'interchangeabl\1'),
+    Typo(r'interger', r'integer'),
+    Typo(r'intermittant', r'intermittent'),
+    Typo(r'intial(ly|)', r'initial\1'),
+    Typo(r'intializ(e|ed|er|ers|ing|ation|)', r'initializ\1'),
+    Typo(r'(in|)vlaid(ate|ated|)', r'\1valid\2'),
+    Typo(r'invokation', r'invocation'),
+    Typo(r'irregardless', r'regardless'),
+    Typo(r'javasscript', r'javascript'),
+    Typo(r'(ack|)knowlege(ment|d|)(s|)', r'\1knowledge\2\3'),
+    Typo(r'aknowle(d|)ge(ment|d|)(s|)', r'acknowledge\2\3'),
+    Typo(r'lamda', r'lambda'),
+    Typo(r'langauge(s|)', r'language\1'),
+    Typo(r'lastest', r'latest'),
+    Typo(r'lealaps', r'laelaps'),
+    Typo(r'lenght(s|)', r'length\1'),
+    Typo(r'libasis+[tan]+', r'libassistant'),
+    Typo(r'libary', r'library'),
+    Typo(r'listner', r'listener'),
+    Typo(r'locaiton(s|)', r'location\1'),
+    Typo(r'looup', r'lookup'),
+    Typo(r'managment', r'management'),
+    Typo(r'maxumum', r'maximum'),
+    Typo(r'maintenace', r'maintenance'),
+    Typo(r'meatadata|meatdata', r'metadata'),
+    Typo(r'medatata', r'metadata'),
+    Typo(r'migation', r'migration'),
+    Typo(r'moniter(s|ed|ing|able|)', r'monitor\1'),
+    Typo(r'mutiple|mulitple', r'multiple'),
+    Typo(r'neccessary', r'necessary'),
+    Typo(r'necesar(y|ily)', r'necessar\1'),
+    Typo(r'newstand', r'newsstand'),
+    Typo(r'nulltpr', r'nullptr'),
+    Typo(r'oc+ur+(a|e)nce', r'occurrence'),
+    Typo(r'occur(ed|ing)', r'occurr\1'),
+    Typo(r'offical', r'official'),
+    Typo(r'or(gi|ig)nal', r'original'),
+    Typo(r'ove(r|)riden', r'overridden'),
+    Typo(r'overide', r'override'),
+    Typo(r'ove(r|)rite', r'overwrite'),
+    Typo(r'ove(r|)rites', r'overwrites'),
+    Typo(r'ove(r|)rit(t|)en', r'overwritten'),
+    Typo(r'pacakge', r'package'),
+    Typo(r'paramter(s|)', r'parameter\1'),
+    Typo(r'path[aei]logical', r'pathological'),
+    Typo(r'permanant', r'permanent'),
+    Typo(r'platfrom|paltform|platfom|patform|plaftorm', r'platform'),
+    Typo(r'peroid(ic|ical|ically|)', r'period\1'),
+    Typo(r'persistant', r'persistent'),
+    Typo(r'pipeling|pipline', r'pipeline'),
+    Typo(r'postion', r'position'),
+    Typo(r'postive', r'positive'),
+    Typo(r'preceed(ed|ing)', r'preced\1'),
+    Typo(r'precodition(s|)', r'precondition\1'),
+    Typo(r'pref+er+ed', r'preferred'),
+    Typo(r'pref+er+[ae]nce(s|)', r'preference\1'),
+    Typo(r'presedence', r'precedence'),
+    Typo(r'(p|)revew|reviw(ed|s|)', r'\1review\2'),
+    Typo(r'proably', r'probably'),
+    Typo(r'procede', r'proceed'),
+    Typo(r'proces(ing|ed|es)', r'process\1'),
+    Typo(r'produciton', r'production'),
+    Typo(r'programatic(ally|)', r'programmatic\1'),
+    Typo(r'prohibitted', r'prohibited'),
+    Typo(r'propogat(ion|ed|es|e)', r'propagat\1'),
+    Typo(r'protocal', r'protocol'),
+    Typo(r'proudction', r'production'),
+    Typo(r'psuedo', r'pseudo'),
+    Typo(r'pubicly', r'publicly'),
+    Typo(r'publically', r'publicly'),
+    Typo(r'\bputing\b', r'putting'),
+    Typo(r'qualificaiton(s|)', r'qualification\1'),
+    Typo(r'realease', r'release'),
+    Typo(r'recieve', r'receive'),
+    Typo(r'recomend(ation|ations|s|ing|)', r'recommend\1'),
+    Typo(r'recrod', r'record'),
+    Typo(r'relase', r'release'),
+    Typo(r'relevent', r'relevant'),
+    Typo(r'(ir|)relvant', r'\1relevant'),
+    Typo(r'reponse|respone', r'response'),
+    Typo(r'reponsibilit(y|ies)', r'responsibilit\1'),
+    Typo(r'reqeust(s|ed|ing|)', r'request\1'),
+    Typo(r'reserach', r'research'),
+    Typo(r'(re|)sopurce', r'\1source'),
+    Typo(r'responsabilit(y|ies)', r'responsibilit\1'),
+    Typo(r'resopnse', r'response'),
+    Typo(r'resovl(e[sd]?|ing)', r'resolv\1'),
+    Typo(r'retre?ive(d|s|)', r'retrieve\1'),
+    Typo(r'retrival', r'retrieval'),
+    Typo(r'runnning', r'running'),
+    Typo(r'saftey', r'safety'),
+    Typo(r'san[ietz]+r', r'sanitizer'),
+    Typo(r'seconary', r'secondary'),
+    Typo(r'seperate(d|s|)', r'separate\1'),
+    Typo(r'serach', r'search'),
+    Typo(r'shoppping', r'shopping'),
+    Typo(r'similiar', r'similar'),
+    Typo(r'singals', r'signals'),
+    Typo(r'snasphot', r'snapshot'),
+    Typo(r'specical', r'special'),
+    Typo(r'speficying', r'specifying'),
+    Typo(r'standart', r'standard'),
+    Typo(r'starteg(ies|y)', r'strateg\1'),
+    Typo(r'statment', r'statement'),
+    Typo(r'stich(ed|ing)', r'stitch\1'),
+    Typo(r'stirng', r'string'),
+    Typo(r'stroage', r'storage'),
+    Typo(r'su+c+e+s+(ful|es|)', r'success\1'),
+    Typo(r'su+p+e+r+o+t+', r'superroot'),
+    Typo(r'substract', r'subtract'),
+    Typo(r'successfuly', r'successfully'),
+    Typo(r'sufix', r'suffix'),
+    Typo(r'sugest', r'suggest'),
+    Typo(r'sycn', r'sync'),
+    Typo(r'syncronize', r'synchronize'),
+    Typo(r'threre(after|fore|)', r'there\1'),
+    Typo(r'threshould|thershold', r'threshold'),
+    Typo(r'throught', r'through'),
+    Typo(r'thumnail', r'thumbnail'),
+    Typo(r'transfer(ing|ed)', r'transferr\1'),
+    Typo(r'transation', r'transaction'),
+    Typo(r'tricoder', r'tricorder'),
+    Typo(r'uderlying', r'underlying'),
+    Typo(r'udpate', r'update'),
+    Typo(r'undefiend', r'undefined'),
+    Typo(r'unkown', r'unknown'),
+    Typo(r'virutal', r'virtual'),
+    Typo(r'weigth(s|ing|)', r'weight\1'),
+    Typo(r'wether', r'whether'),
+    Typo(r'wierd(ness|o|)', r'weird\1'),
+    Typo(r'worfklow', r'workflow'),
+    Typo(r'writen', r'written'),
+    Typo(r'writting', r'writing'),
+    Typo(r'zweiback', r'zwieback'),
+    Typo(r'\bGoogle,(\s+)Inc\.?\b', r'Google\1Inc.'),
+    Typo(r'\band\s+and\b', r'and'),
+    Typo(r'\bif\s+if\b', r'if'),
+    Typo(r'\bit\s+it\b', r'it'),
+    Typo(r'\bof\s+of\b', r'of'),
+    Typo(r'\bthe\s+its\b', r'its'),
+    Typo(r'\bthe\s+the\b', r'the'),
+    Typo(r'\bwith\s+with\b', r'with'),
+    Typo(r'\bObjective\sC\b', r'Objective-C'),
+    Typo(r'http(s|):\/\/\/', r'http\1://'),
+    Typo(r'https:\/\/([[:alnum:]]+)\/', r'http://\1/'),
+    Typo(r'ouptut|ouput|outout|OUPTUT|OUPUT', r'output'),
+    Typo(r'A-za-z', r'A-Za-z'),
+    Typo(r'a-zA-z', r'a-zA-Z'),
+    Typo(
+        r'^(|.*[^TtRrSsEe])(TOOO|TOD0|TODI|TODP|TOO|TOSO|TODDO|TOOD|TOCO|TOTO|ODO|TDOO|TODOD|TOFO|TDDO|TODD|TOD|TODOO|TDO|TODL)\W?\(\s*(([a-z]{1,14}@?)|(b.?\d+))\s*\)',
+        r'\1TODO(\3)'),
+]
diff --git a/cq/scripts/resources/kythe_step/vnames.json b/cq/scripts/resources/kythe_step/vnames.json
new file mode 100644
index 0000000..bf04ad4
--- /dev/null
+++ b/cq/scripts/resources/kythe_step/vnames.json
@@ -0,0 +1,24 @@
+[
+  {
+    "pattern": "out_fuchsia_arm64_user/Release/gen/(.*)",
+    "vname": {
+      "corpus": "eureka-internal.googlesource.com/codesearch",
+      "root": "out_fuchsia/release/gen",
+      "path": "@1@"
+    }
+  },
+  {
+    "pattern": "chromecast/internal/(.*)",
+    "vname": {
+      "corpus": "eureka-internal.googlesource.com/codesearch",
+      "path": "cast/internal/@1@"
+    }
+  },
+  {
+    "pattern": "(.*)",
+    "vname": {
+      "corpus": "eureka-internal.googlesource.com/codesearch",
+      "path": "@1@"
+    }
+  }
+]
\ No newline at end of file
diff --git a/cq/scripts/slave/__init__.py b/cq/scripts/slave/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cq/scripts/slave/__init__.py
diff --git a/cq/scripts/slave/accelerator/__init__.py b/cq/scripts/slave/accelerator/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cq/scripts/slave/accelerator/__init__.py
diff --git a/cq/scripts/slave/accelerator/no_accelerator.py b/cq/scripts/slave/accelerator/no_accelerator.py
new file mode 100644
index 0000000..59c92a8
--- /dev/null
+++ b/cq/scripts/slave/accelerator/no_accelerator.py
@@ -0,0 +1,31 @@
+
+class NoAccelerator(object):
+  @property
+  def make_flags(self):
+    return []
+
+  @property
+  def build_chromecast_flags(self):
+    return []
+
+  @property
+  def gn_flags(self):
+    return []
+
+  @property
+  def environment_variables(self):
+    return {}
+
+  def setup_steps(self, step_kwargs):
+    del step_kwargs
+    return []
+
+  def tear_down_steps(self, step_kwargs):
+    del step_kwargs
+    return []
+
+  def setup_mocks(self, step):
+    pass
+
+  def tear_down_mocks(self, step):
+    pass
\ No newline at end of file
diff --git a/cq/scripts/slave/accelerator/rbe_accelerator.py b/cq/scripts/slave/accelerator/rbe_accelerator.py
new file mode 100644
index 0000000..ef2e883
--- /dev/null
+++ b/cq/scripts/slave/accelerator/rbe_accelerator.py
@@ -0,0 +1,39 @@
+from slave.step import reproxy_start_step
+from slave.step import reproxy_stop_step
+from helpers import rbe_utils
+
+
+class Accelerator(object):
+  @property
+  def make_flags(self):
+    if rbe_utils.is_installed():
+      return ['USE_RBE=1', 'ENABLE_RBE_BOOTSTRAP=0', 'NUM_JOBS=300']
+    return []
+
+  @property
+  def build_chromecast_flags(self):
+    if rbe_utils.is_installed():
+      return ['--use_rbe']
+    return []
+
+  @property
+  def gn_flags(self):
+    if rbe_utils.is_installed():
+      return ['use_rbe=true', 'enable_rbe_bootstrap=false']
+    return []
+
+  @property
+  def environment_variables(self):
+    return {}
+
+  def setup_steps(self, step_kwargs):
+    return [reproxy_start_step.ReproxyStartStep(**step_kwargs)]
+
+  def tear_down_steps(self, step_kwargs):
+    return [reproxy_stop_step.ReproxyStopStep(**step_kwargs)]
+
+  def setup_mocks(self, step):
+    pass
+
+  def tear_down_mocks(self, step):
+    pass
diff --git a/cq/scripts/slave/base_recipe.py b/cq/scripts/slave/base_recipe.py
new file mode 100644
index 0000000..250a55a
--- /dev/null
+++ b/cq/scripts/slave/base_recipe.py
@@ -0,0 +1,164 @@
+"""Base class for build recipes."""
+import abc
+import logging
+import os
+from pathlib import Path
+import pprint
+import six
+from typing import Any, Dict, Optional
+
+from helpers import accelerator_utils
+from slave.accelerator import no_accelerator
+from slave.step import clean_sync_step
+from slave.step import git_clean_step
+from slave.step import license_manager_step
+from slave.step import make_clean_step
+from slave.step import platform_info_step
+
+
+LOGGER = logging.getLogger(__name__)
+
+class BuildNameNotSupportedError(Exception):
+  """Raised when a recipe does not support the given build_name."""
+
+
+class BaseRecipe(six.with_metaclass(abc.ABCMeta, object)):
+  """Base class for recipes."""
+
+  def __init__(self,
+               executor: 'Executor',
+               eureka_root: str,
+               enable_build_accelerator: bool=False,
+               properties: Optional[Dict[str, Any]]=None,
+               **kwargs):
+    """Base constructor.
+
+    Args:
+      **kwargs: The kwargs to pass to individual steps.
+    """
+    LOGGER.info("Creating recipe %s", type(self))
+    LOGGER.info("Properties: %s", pprint.pformat(properties))
+    LOGGER.info("Eureka root: %s", eureka_root)
+    self._eureka_root = Path(eureka_root)
+    self._properties = properties or {}
+    self._build_accelerator = (
+        accelerator_utils.get_build_accelerator()
+        if enable_build_accelerator else no_accelerator.NoAccelerator())
+    self._executor = executor
+    self._remote_lookup = None
+    self._path_lookup = None
+    self._fix_floats()
+    self._skip_gclient = self._properties.get('skip_gclient', False)
+    self._git_clean = self._properties.get('git_clean', False)
+    self._make_clean = self.build_system == 'catabuilder'
+    self._make_distclean = self._properties.get('make_distclean', False)
+    self._manifest_branch = self._properties.get('manifest_branch')
+    self._patch_project = self._properties.get('patch_project')
+    self._env = self._properties.get('build_env', 'local')
+    self._manifest_url = self._properties.get('manifest_url')
+    self._check_android_license = (
+        self._env != 'local' and kwargs.get('check_android_license', False))
+
+    self._step_kwargs = kwargs.copy()
+    self._step_kwargs['build_accelerator'] = self._build_accelerator
+    self._step_kwargs["executor"] = executor
+    self._step_kwargs["eureka_root"] = self._eureka_root
+    self._step_kwargs["enable_build_accelerator"] = enable_build_accelerator
+    self._step_kwargs["properties"] = properties
+
+  @property
+  def build_system(self):
+    """Returns the name of current build system."""
+    return self._properties.get('build_system', 'local')
+
+  @property
+  def buildername(self):
+    """Returns the buildername of the current build."""
+    return self._properties.get('buildername')
+
+  def get_setup_steps(self):
+    """Returns a list of all steps that should be executed as part of setup.
+
+    Returns:
+      List of slave.base_step.BaseStep commands to be executed.
+    """
+    setup_steps = []
+    setup_steps.append(
+        platform_info_step.PlatformInfoStep(
+            directory='', **self._step_kwargs))
+
+    if not self._skip_gclient:
+      setup_steps.append(
+          clean_sync_step.GclientSyncStep(
+              directory='', **self._step_kwargs))
+
+    if self._check_android_license:
+      setup_steps.append(license_manager_step.AndroidSdkLicenseCheckAccept(
+          directory='', **self._step_kwargs))
+
+    setup_steps.extend(
+        self. _get_build_accelerator().setup_steps(self._step_kwargs))
+
+    if self._make_clean:
+      setup_steps += self.get_clean_steps()
+
+    if self._make_distclean:
+      setup_steps.append(
+          make_clean_step.MakeDistCleanStep(**self._step_kwargs))
+
+    if self._git_clean:
+      setup_steps.append(
+          git_clean_step.GitCleanStep(**self._step_kwargs))
+
+    return setup_steps
+
+  def _get_build_accelerator(self):
+    return self._build_accelerator
+
+  def get_teardown_steps(self):
+    """Returns a list of all steps that should be executed as part of teardown.
+
+    Returns:
+      List of slave.base_step.BaseStep commands to be executed.
+    """
+    return self._get_build_accelerator().tear_down_steps(self._step_kwargs)
+
+  @property
+  def build_number(self):
+    """Construct BUILD_NUMBER string in unified way between all systems.
+
+    This property is matching the same property in the BaseStep, should be
+    altered together.
+
+    Returns:
+      CL number (from 'issue' porperty) for CQ, buildset number for CB or 0
+    """
+    return str(self._properties.get(
+        'issue', self._properties.get('buildset', '0')))
+
+  @abc.abstractmethod
+  def get_steps(self):
+    """Builds a list of all steps that should be executed by this recipe.
+
+    Returns:
+      List of slave.base_step.BaseStep commands to be executed.
+    """
+    return []
+
+  def get_clean_steps(self):
+    """Builds a list of all clean steps that should be executed by this recipe.
+
+    Returns:
+      List of slave.base_step.BaseStep commands to be executed.
+    """
+    return []
+
+  def _fix_floats(self):
+    """Buildbucket is enforcing floats, but we want ints."""
+    for key, value in self._properties.items():
+      if isinstance(value, float):
+        self._properties[key] = int(value)
+
+  def _getcwd(self):
+    """Helper function to make it easy to mock os.getcwd() for tests."""
+    return os.getcwd()
diff --git a/cq/scripts/slave/base_step.py b/cq/scripts/slave/base_step.py
new file mode 100644
index 0000000..937868a
--- /dev/null
+++ b/cq/scripts/slave/base_step.py
@@ -0,0 +1,572 @@
+"""Base class for build steps."""
+import logging
+import multiprocessing
+import os
+import re
+import subprocess
+
+import psutil
+
+from cq.scripts.helpers import git_utils
+from cq.scripts.helpers import timeout
+
+# Preventing running any step longer than this amount of time.
+STEP_MAX_TIME_SECONDS = 2 * 60 * 60
+
+# If it takes a subprocess more than 2 mins to exit, let's assume it's hung
+# and will probably never return properly.
+_SUBPROCESS_TIMEOUT = 2 * 60
+
+
+class StepError(Exception):
+  """Exception raised when a step failure happens."""
+
+
+class GoBlsremoteQuotaError(StepError):
+  """Exception raise for the GoB lsremote quota exceeded error."""
+  ERROR_MSG = 'Short term ls-remote rate limit exceeded'
+
+
+class GoBbandwidthQuotaError(StepError):
+  """Exception raise for the GoB bandwidth quota exceeded error."""
+  ERROR_MSG = 'Short term bandwidth rate limit exceeded'
+
+
+class GoBManifestInvalidRevisionError(StepError):
+  """Exception raise for the GoB invalid manifest revision error."""
+  ERROR_MSG = 'ManifestInvalidRevisionError'
+
+
+class BaseStep:
+  """Base class for build steps.
+
+  Implementers should override the methods below as appropriate.
+
+  Attributes:
+    name: user-visible name of this step.
+    env: representation of build_env build property.
+    data_manager: handles managing data between steps.
+    executor: invokes and interacts with external processes.
+    annotator: receives annotations from steps to communicate to build master.
+    halt_on_failure: whether or not this step's failure should end recipe
+                     execution.
+    allow_list_filter: If specified, a list of regex's to use to filter
+                      files that get linted. Only file names that match
+                      at least one filter in the allow_list will get linted.
+                      An empty list will result in no files getting linted.
+                      If a file matches a allow_list filter and a deny_list
+                      filter, the file will be deny_listed.
+                      (default allows all files)
+    deny_list_filter: If specified, a list of regex's to use to filter files
+                      that get linted. Only file names that do not match
+                      any filter in the deny_list will get linted.
+                      If a file matches a allow_list filter and a deny_list
+                      filter, the file will be deny_listed.
+                      (default allows all files)
+  """
+
+  def __init__(self,
+               name: str,
+               executor=None,
+               annotator=None,
+               data_manager=None,
+               halt_on_failure=False,
+               properties=None,
+               directory=None,
+               timeout_secs=STEP_MAX_TIME_SECONDS,
+               result_on_timeout=False,
+               allow_list_filter=None,
+               deny_list_filter=None,
+               prerun_data=None,
+               build_accelerator=None, **kwargs):
+    assert name
+    assert executor
+    assert data_manager
+    assert build_accelerator
+    assert isinstance(halt_on_failure, bool)
+    if allow_list_filter:
+      assert isinstance(allow_list_filter, list)
+    if deny_list_filter:
+      assert isinstance(deny_list_filter, list)
+
+    self._name = name
+    self._executor = executor
+    self._annotator = annotator
+    self._data_manager = data_manager
+    self._non_blocking_subprocesses = []
+    self._halt_on_failure = halt_on_failure
+    self._properties = properties
+    self._directory = directory
+    self._project_directory = None
+    self._project_remote = None
+    if not isinstance(timeout_secs, int):
+      timeout_secs = 0
+    self._timeout_secs = timeout_secs
+    self._result_on_timeout = result_on_timeout
+    self._run_results = {
+        'name': self.name,
+        'exit_code': 0,
+        'files': {},
+        'logs': {},
+        'valid': False
+    }
+    if prerun_data is None:
+      prerun_data = {}
+    else:
+      assert isinstance(prerun_data, dict)
+      self._run_results['valid'] = True
+    self._run_results.update(prerun_data)
+
+    if allow_list_filter is None:
+      allow_list_filter = ['.*']
+    if deny_list_filter is None:
+      deny_list_filter = []
+    self._allow_list_filter = [re.compile(regex) for regex in allow_list_filter]
+    self._deny_list_filter = [re.compile(regex) for regex in deny_list_filter]
+    self._last_subprocess_cmd = None
+    self._build_accelerator = build_accelerator
+
+    _ = kwargs
+
+  @property
+  def results(self):
+    """Returns the result of this step run."""
+    return self._run_results
+
+  @property
+  def name(self):
+    """Returns the name of this step."""
+    return self._name
+
+  @property
+  def build_system(self):
+    """Returns the name of current build system."""
+    return self.get_property('build_system', 'local')
+
+  @property
+  def env(self):
+    return self.get_property('build_env', 'local')
+
+  @property
+  def remote(self):
+    """Returns the remote to use for this step."""
+    if not self._project_remote:
+      self._project_remote = self.get_project_remote(self.patch_project)
+    return self._project_remote
+
+  @property
+  def directory(self):
+    """Returns the directory to use for this step."""
+    if self._directory is not None:
+      return self._directory
+    if not self._project_directory:
+      if self.patch_project:
+        self._project_directory = self.get_project_path(self.patch_project)
+      else:
+        self._project_directory = ''
+    return self._project_directory
+
+  @property
+  def build_accelerator(self):
+    return self._build_accelerator
+
+  def get_property(self, name, default=None):
+    """Returns the requested property value, or default if not found."""
+    if not self._properties:
+      return default
+    return self._properties.get(name, default)
+
+  @property
+  def depends_on_list(self):
+    """Returns the list of Depends-On information.
+
+    Each item in the list is a dict of the form:
+    {
+     'gob_host': <Gerrit on borg host name>
+     'project': <Project name, should match a name in the repo manifest>
+     'change_number': <CL #>
+     'patchset_number': <PS #>
+    }
+
+    Returns:
+      List of depends-on dicts.
+    """
+    return self.get_property('depends_on', default=[])
+
+  @property
+  def build_number(self):
+    """Construct BUILD_NUMBER string in unified way between all systems.
+
+    This property is matching the same property in the BaseRecipe, should be
+    altered together.
+
+    Returns:
+      CL number (from 'issue' porperty) for CQ, buildset number for CB or 0
+    """
+    return str(self.get_property(
+        'issue', default=self.get_property('buildset', default='0')))
+
+  @property
+  def patchset(self):
+    """Returns the patchset number for this step."""
+    return self.get_property('patchset', default='0')
+
+  @property
+  def patch_project(self):
+    """Returns the patch_project to use for this step."""
+    return self.get_property('patch_project')
+
+  @property
+  def manifest_branch(self):
+    """Returns the manifest branch to use for this step."""
+    return self.get_property('manifest_branch')
+
+  def allow_listed(self, filename):
+    return any(re.search(regex, filename) for regex in self._allow_list_filter)
+
+  def deny_listed(self, filename):
+    return any(re.search(regex, filename) for regex in self._deny_list_filter)
+
+  def changed_files(self, file_extensions=None):
+    changed_files = git_utils.changed_files(
+        self, directory=self.directory, file_extensions=file_extensions)
+    return [f for f in changed_files
+            if self.allow_listed(f) and not self.deny_listed(f)]
+
+  def get_num_jobs(self, multiplier=2):
+    """Returns the number of jobs this step can use.
+
+    Uses a heuristic to figure out the number of parallel jobs supported by
+    the build machine. Currently this is just 2x the number of CPU cors.
+
+    Args:
+      multiplier: The multiplier to use.
+
+    Returns:
+      The number of jobs to use.
+    """
+    return multiprocessing.cpu_count() * multiplier
+
+  def exec_subprocess(self,
+                      command,
+                      check_output=False,
+                      cwd=None,
+                      *args,
+                      **kwargs):  # pylint: disable=keyword-arg-before-vararg
+    """Execute a command.
+
+    Args:
+      command: The command to execute.
+      check_output: Whether or not to raise a CalledProcessError on non-zero
+        return code.
+      cwd: The current working directory to use for the subprocess.
+        Defaults to os.getcwd()
+      *args: positional params.
+      **kwargs: Key/Value dictionary params.
+    Returns:
+      Either just stderr if check_output=True, or a tuple of
+      (returncode, stdout, stderr)
+    """
+    self._last_subprocess_cmd = command
+
+    returncode, stdout, stderr = \
+      self._executor.subprocess(command, *args, cwd=cwd, **kwargs)
+
+    if check_output:
+      if returncode != 0:
+        full_output = stdout
+        if stderr:
+          full_output += '\nstderr:\n' + stderr
+        raise subprocess.CalledProcessError(returncode,
+                                            cmd=command,
+                                            output=full_output)
+
+      return stdout
+    return returncode, stdout, stderr
+
+  def exec_subprocess_chain(self, subprocess_chain):
+    """Execute a chain of commands, piping output between processes."""
+    msg = ['Chain of commands, piping output between processes:']
+    commands = msg + [' '.join(command[0]) for command in subprocess_chain]
+    commands = [cmd.strip() + '\n' for cmd in commands]
+    self._last_subprocess_cmd = commands
+    return self._executor.subprocess_chain(subprocess_chain)
+
+  def exec_non_blocking_subprocess(self, *args, **kwargs):
+    subprocess, log_file_descriptor, log_file_path = (
+        self._executor.non_blocking_subprocess(*args, **kwargs))
+    self._non_blocking_subprocesses.append((subprocess, log_file_descriptor))
+    return subprocess, log_file_descriptor, log_file_path
+  
+  def start_subprocess(self, *args, **kwargs):
+    """Direct call to the subprocess.Popen.
+
+    Args:
+      stdin: A file stream, subprocess.PIPE, or None to read stdin from.
+      stdout: A file stream or subprocess.PIPE to redirect stdout.
+      stderr: A file stream or subprocess.PIPE to redirect stderr.
+      *args: passed to subprocess.Popen
+      **kwargs: passed to subprocess.Popen;
+                optionally, quiet=True can be passed as a kwarg,
+                in which case stdout/stderr from the subprocess
+                will not be written to file. This kwarg will not
+                get passed to subprocess.Popen.
+
+    Returns:
+      Tuple of (subprocess, boolean indicating if subprocess output needs to be
+                saved.)
+    """
+    return self._executor.start_subprocess(*args, **kwargs)
+ 
+  def get_running_subprocess_pids(self):
+    current_running_processes = []
+    for subprocess, _ in self._non_blocking_subprocesses:
+      if subprocess.returncode is None:
+        current_running_processes.append(subprocess.pid)
+    return current_running_processes
+
+  def kill_all_blocking_subprocesses(self):
+    """Killing all current blocking subprocesses."""
+    msg = u'Last cmd run by the executor: {}'.format(
+        str(self._last_subprocess_cmd))
+
+    # Print all current running processes
+    self.exec_subprocess(['ps', '-efH'])
+
+    for subprocess in self._executor.get_blocking_subprocesses():
+      pid = subprocess.pid
+      if not psutil.pid_exists(pid):
+        logging.info('[Kill all blocking] Process %d started at %d and is no '
+                     'longer running, skip', pid, subprocess.start)
+        continue
+      subprocess_info = psutil.Process(pid)
+
+      logging.info('[Kill all blocking] Process %d, (%s) started at %d. '
+                   'Communicating with process...',
+                   pid, ' '.join(subprocess_info.cmdline()), subprocess.start)
+      stdout = None
+      stderr = None
+      try:
+        stdout, stderr = subprocess.communicate(timeout=_SUBPROCESS_TIMEOUT)
+        logging.info('[Kill all blocking] Process %d: exited with '
+                     'returncode = %d.', pid, subprocess.returncode)
+      except timeout.TimeoutException as e:
+        # Due to a quirkiness of Python imports, we cannot catch
+        # subprocess42.TimeoutExpired directly, so we are catching its base
+        # class instead. However, we need to check for the attributes existence
+        # to make sure this is indeed subprocess42.TimeoutExpired.
+        if hasattr(e, 'output'):
+          stdout = e.output
+        if hasattr(e, 'stderr'):
+          stderr = e.stderr
+        logging.exception('Timeout (%d s) expired while waiting for the '
+                          'process to finish. Killing process: %d',
+                          _SUBPROCESS_TIMEOUT, pid)
+        subprocess.kill()
+        subprocess.wait()
+      finally:
+        if stdout:
+          logging.info('[Kill all blocking] Stdout size for process %d: %d.',
+                       pid, len(stdout))
+        if stderr:
+          logging.info('[Kill all blocking] Stderr size for process %d: %d.',
+                       pid, len(stderr))
+
+
+  def kill_all_non_blocking_subprocesses(self):
+    """Killing all current non-blocking subprocesses."""
+    for subprocess, log_file_descriptor in self._non_blocking_subprocesses:
+      try:
+        subprocess.poll()
+        if subprocess.returncode is None:
+          logging.info('Killing process: %d', subprocess.pid)
+          subprocess.kill()
+      except:  # pylint: disable=bare-except
+        logging.exception('Killing process failed.')
+      try:
+        logging.info('Closing file handler %d', log_file_descriptor)
+        os.close(log_file_descriptor)
+      except:  # pylint: disable=bare-except
+        logging.exception('Closing file handler failed.')
+
+  def exec_read_file(self, file_path):
+    return self._executor.read_file(file_path)
+
+  def glob_files(self, glob_expr):
+    return self._executor.glob_files(glob_expr)
+
+  def get_tmp_dir(self):
+    return self._executor.get_tmp_dir()
+
+  def get_new_tmp_file(self):
+    return self._executor.get_new_tmp_file()
+
+  def write_temp_file(self, contents: str):
+    """Write a text string to a temporary file."""
+    return self._executor.write_temp_file(contents)
+
+  def add_review(self, review):
+    """Adds |review| to the review comment.
+
+    Args:
+      review: A dict with optional 'message' and 'comments' keys.
+              If 'message' is present, it will be added as a review comment.
+              If 'comments' is present, they will be added as file comments.
+
+              'message': A string to be added to the review message.
+              'comments': A dict of comments in the form of
+                          {<filename>: [{'line': <line #>,
+                                         'message': <file comment>},
+                                        {'line': <line #>,
+                                         'message': <file comment>},
+                                        ...
+                                       ],
+                           <filename>: [...],
+                           ...
+                          }
+                         Note that the 'line' field is optional, and if not
+                         provided will result in a file-level comment instead
+                         of a line-specific comment.
+                         To add a comment to the commit message, use
+                         '/COMMIT_MSG' as the filename.
+    """
+    if 'message' in review:
+      assert isinstance(review['message'], str)
+      if self._annotator:
+        self._annotator.add_message(review['message'])
+    if 'comments' in review:
+      assert isinstance(review['comments'], dict)
+      if self._annotator:
+        self._annotator.add_comments(review['comments'])
+
+  def get_cq_root(self):
+    """Returns the absolute path to the root of the CQ src."""
+    return self._executor.get_cq_root()
+
+  def get_gcs_dir(self):
+    """This directory will be uploaded to GCS when a recipe finishes."""
+    return self._executor.get_gcs_dir()
+
+  def set_build_property(self, name, value):
+    """Sets a build property for this step to return to the builder.
+
+    Args:
+      name: property name (must be a string)
+      value: property value (must be JSON-serializable)
+    """
+    assert isinstance(name, str)
+    assert value is not None
+    self._properties[name] = value
+    if self._annotator:
+      self._annotator.set_build_property(name, value)
+
+  def halt_on_failure(self):
+    """Returns whether this step's failure should end the recipe."""
+    return self._halt_on_failure
+
+  def set_halt_on_failure(self):
+    """Forces step failure to end the recipe run."""
+    self._halt_on_failure = True
+
+  def get_name(self):
+    """Returns the name of this step, used for status display and flow control.
+
+    Returns:
+      The name of this step.
+    """
+    return self._name
+
+  def get_timeout(self):
+    """Returns the length of time, in seconds, this step is allowed to run."""
+    return self._timeout_secs
+
+  def result_on_timeout(self):
+    """Returns the result value to use if this step times out."""
+    return self._result_on_timeout
+
+  def get_step_data(self, key, default=None):
+    """Retrieves step data from a previous step.
+
+    Args:
+      key: Unique name the step data was indexed by.
+      default: Default return value if the key is not found.
+
+    Returns:
+      Data associated with the given key.
+    """
+    return self._data_manager.get_data(key, default=default)
+
+  def add_step_data(self, key, value, replace=False):
+    """Provides step data for future steps.
+
+    Args:
+      key: Unique name to index this step data.
+      value: Actual data to provide to future steps.
+      replace: whether |value| can replace previously stored data in |key|.
+               (defaults to False)
+    """
+    self._data_manager.add_data(key, value, replace)
+
+  def clear_project_info(self):
+    """Clears the project info to be reset in the next run."""
+    return self._data_manager.clear_project_info()
+
+  def contains_project(self, project):
+    """Boolean if the given project is included in the repo client."""
+    return self._data_manager.contains_project(project)
+
+  def ensure_project_info(self):
+    """Populate the project info."""
+    return self._data_manager.ensure_project_info()
+
+  def get_project_path(self, project):
+    """Returns the path to the project.
+
+    Args:
+      project: The name of the project (usually patch_project) to lookup.
+
+    Returns:
+      The relative path from the base checkout to the project.
+    """
+    return self._data_manager.get_project_path(project)
+
+  def get_project_path_lookup_table(self):
+    """Returns the project path lookup table."""
+    return self._data_manager.get_project_path_lookup_table()
+
+  def get_project_remote(self, project):
+    """Returns the remote for the project.
+
+    Args:
+      project: The name of the project (usually patch_project) to lookup.
+
+    Returns:
+      The remote for the project.
+    """
+    return self._data_manager.get_project_remote(project)
+
+  def get_project_revision(self, project):
+    """Returns the revision for the project.
+
+    Args:
+      project: The name of the project (usually patch_project) to lookup.
+
+    Returns:
+      The revision for the project.
+    """
+    return self._data_manager.get_project_revision(project)
+
+  def run(self) -> bool:
+    """Synchronously executes this step of the recipe.
+
+    Returns:
+      Whether or not the step succeeded.
+    """
+    raise NotImplementedError("Subclasses should have implemented this.")
+
+  def add_mock_executions(self, generator):
+    """Adds mock executions to the generator."""
+
+  def _getcwd(self):
+    """Helper function to make it easy to mock os.getcwd() for tests."""
+    return os.getcwd()
diff --git a/cq/scripts/slave/recipe/__init__.py b/cq/scripts/slave/recipe/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cq/scripts/slave/recipe/__init__.py
diff --git a/cq/scripts/slave/recipe/cast_service_builder.py b/cq/scripts/slave/recipe/cast_service_builder.py
new file mode 100644
index 0000000..528f4f6
--- /dev/null
+++ b/cq/scripts/slave/recipe/cast_service_builder.py
@@ -0,0 +1,67 @@
+"""Recipe for building cast_service standalone executable."""
+import os
+
+from slave import base_recipe
+from slave.step import gn_step
+from slave.step import ninja_step
+from slave.step import sanitizer_unittest_step
+
+BUILD_CONFIGS = {
+    'cast_service_x64' : {
+        'build_args_product': 'displayassistant_x64_eng',
+        'build_args': 'enable_standalone_cast_service=true',
+    }
+}
+
+
+
+def GetValidBuildNames():
+  return list(BUILD_CONFIGS.keys())
+
+def CreateRecipe(build_name: str, **kwargs):
+  config = BUILD_CONFIGS[build_name]
+  return CastServiceRecipe(build_name, config, **kwargs)
+
+class CastServiceRecipe(base_recipe.BaseRecipe):
+  """This recipe builds the cast_service standalone executable."""
+  def __init__(self, build_name, config, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._config = config
+    self._build_name = build_name
+
+  def get_steps(self):
+    steps = []
+    cwd = os.getcwd()
+    setup_steps = self.get_setup_steps()
+    chromium_root = setup_steps[0].get_project_path('chromium/src')
+    out_dir = os.path.join(chromium_root,
+                           'out_{}'.format(self._build_name),
+                           'release')
+
+    gn_gen_step = gn_step.GnGenStep(
+        build_args_product=self._config['build_args_product'],
+        build_args=[self._config['build_args']],
+        out_dir=out_dir,
+        halt_on_failure=True,
+        cwd=cwd,
+        **self._step_kwargs
+    )
+    steps.append(gn_gen_step)
+    gn_args_step = gn_step.GnArgsStep(
+        out_dir=out_dir,
+        cwd=cwd,
+        halt_on_failure=True,
+        **self._step_kwargs
+    )
+    steps.append(gn_args_step)
+
+    ninja_build_step = ninja_step.NinjaStep(
+        out_dir=out_dir,
+        target='cast_service',
+        cwd=cwd,
+        jobs=64,
+        halt_on_failure=True,
+        **self._step_kwargs)
+    steps.append(ninja_build_step)
+    return steps
diff --git a/cq/scripts/slave/recipe/cast_shell_guitar_e2e_test_runner.py b/cq/scripts/slave/recipe/cast_shell_guitar_e2e_test_runner.py
new file mode 100644
index 0000000..fba8d41
--- /dev/null
+++ b/cq/scripts/slave/recipe/cast_shell_guitar_e2e_test_runner.py
@@ -0,0 +1,77 @@
+# Lint as: python3
+"""Recipie for running oxygen guitar end to end tests for cast shells."""
+
+import os
+from slave import base_recipe
+from slave.recipe import clang_builder
+from slave.step import cast_shell_step
+from slave.step import gcs_upload_step
+
+SHELL_TO_BUILD = 'audioassistant_grte_eng'
+GCS_BUCKET = 'nest-virtualhome-release'
+GCS_UPLOAD_PATH = ("google3/googledata/chrome/"
+                   "dongle/test/cast_shell/"
+                   "cast_shell_binary/chirp/")
+CQ_BUILD_PREFIX = 'cq'
+
+
+def GetValidBuildNames():
+  return ['guitar_e2e_test_assistant']
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  if build_name != 'guitar_e2e_test_assistant':
+    raise base_recipe.BuildNameNotSupportedError(build_name)
+  return CastShellGuitarE2ERecipe(**kwargs)
+
+
+class CastShellGuitarE2ERecipe(base_recipe.BaseRecipe):
+  """Recipe to run e2e tests on a repository."""
+
+  def __init__(self,
+      **kwargs):
+    super().__init__(
+        enable_build_accelerator=True,
+        **kwargs)
+
+  def get_steps(self):
+    build_shell_config = clang_builder.BUILD_CONFIGS['audioassistant_grte_eng']
+
+    build_branding = build_shell_config.get('build_branding', None)
+    build_arch = build_shell_config.get('build_arch', None)
+    build_product = build_shell_config.get('build_product', None)
+    build_flavor = build_shell_config.get('build_flavor', None)
+    enable_assistant = build_shell_config.get('enable_assistant', False)
+    build_args_official = build_shell_config.get('build_args_official', None)
+    grte_compatible = build_shell_config.get('grte_compatible', False)
+    build_args_product = build_shell_config.get('build_args_product', None)
+
+    steps = []
+
+    cast_shell_build_step = cast_shell_step.CastShellStep(
+        build_branding, build_arch, build_product,
+        build_flavor, enable_assistant,
+        build_args_official=build_args_official,
+        code_coverage=False, grte_compatible=grte_compatible,
+        build_args_product=build_args_product, halt_on_failure=True,
+        sanitizer=None, archive_to_tmp=False, skip_create_zip=True,
+        strip_binaries=True, **self._step_kwargs)
+    steps.append(cast_shell_build_step)
+    outdir = cast_shell_build_step.out_dir
+    outdir = os.path.basename(outdir)
+    print('Out Dir', outdir)
+    gcs_upload_file_step = gcs_upload_step.GcsUploadStep(
+        gcs_bucket=GCS_BUCKET,
+        gcs_path=GCS_UPLOAD_PATH
+                + CQ_BUILD_PREFIX
+                + self.build_number + "_"
+                + str(cast_shell_build_step.patchset) + "/",
+        src_path=os.path.join(
+            cast_shell_build_step.get_gcs_dir(),
+            outdir) + "/",
+        name='upload to nest-virtualhome-release',
+        **self._step_kwargs)
+    steps.append(gcs_upload_file_step)
+
+    return steps
diff --git a/cq/scripts/slave/recipe/catabuilder_e2e_mock.py b/cq/scripts/slave/recipe/catabuilder_e2e_mock.py
new file mode 100644
index 0000000..dc243d2
--- /dev/null
+++ b/cq/scripts/slave/recipe/catabuilder_e2e_mock.py
@@ -0,0 +1,83 @@
+"""Recipe for Catabuilder E2E tests."""
+import enum
+import json
+import logging
+import os
+from typing import Dict, List, Union
+
+from slave import base_recipe
+from slave import base_step
+
+
+@enum.unique
+class Status(enum.Enum):
+  PASS = 'PASSES'
+  FAIL = 'FAILS'
+
+
+_RESULTS_FILE = 'results.json'
+
+
+def GetValidBuildNames() -> List[str]:
+  return [
+      'catabuilder-e2e-mock-1',
+      'catabuilder-e2e-mock-2',
+      'catabuilder-e2e-mock-3',
+  ]
+
+
+def CreateRecipe(build_name: str, **kwargs) -> 'CatabuilderE2EMockRecipe':
+  """Defines how this recipe gets built."""
+  return CatabuilderE2EMockRecipe(build_name, **kwargs)
+
+
+class CatabuilderE2EMockRecipe(base_recipe.BaseRecipe):
+  """Recipe to execute e2e fake build."""
+
+  def __init__(self, builder_name, **kwargs):
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+    self._builder_name = builder_name
+
+  def get_steps(self) -> List['CatabuilderE2EMockStep']:
+    return [
+        CatabuilderE2EMockStep(
+            builder_name=self._builder_name, **self._step_kwargs),
+    ]
+
+
+class CatabuilderE2EMockStep(base_step.BaseStep):
+  """Step that does the verification of the expected CB behavior."""
+
+  def __init__(self, builder_name, **kwargs):
+    base_step.BaseStep.__init__(self, name='e2e_mock', **kwargs)
+    self._builder_name = builder_name
+
+  def run(self) -> bool:
+    results = self._process_instructions()
+    self._save_results_to_gcs_dir(results)
+    outcome = self.get_property('outcome')
+    self.set_build_property('e2e_tests', 'yes')
+    if outcome == Status.PASS.value:
+      return True
+    if outcome == Status.FAIL.value:
+      return False
+    raise ValueError('Status must be set in the "outcome" property.')
+
+  def _process_instructions(self) -> Dict[str, Union[str, int, bool]]:
+    instruction_project = self.get_property('project')
+    project_path = self.get_project_path(instruction_project)
+    instruction_filename = self.get_property('instructions_file')
+    instruction_path = os.path.join(project_path, instruction_filename)
+    logging.info('Opening instruction file: %s', instruction_path)
+    with open(instruction_path, 'r') as instruction_file:
+      instructions = json.load(instruction_file)
+      assert isinstance(instructions, dict)
+      logging.info('Instruction file content: %s', instructions)
+      return instructions
+
+  def _save_results_to_gcs_dir(self, results) -> None:
+    gcs_dir = self._executor.get_gcs_dir()
+    results_path = os.path.join(gcs_dir, _RESULTS_FILE)
+    with open(results_path, 'w') as results_file:
+      logging.info('Writing results to: %s', results_path)
+      json.dump(results, results_file, indent=2, sort_keys=True)
diff --git a/cq/scripts/slave/recipe/catatester_binaries_runner.py b/cq/scripts/slave/recipe/catatester_binaries_runner.py
new file mode 100644
index 0000000..4ef2a4e
--- /dev/null
+++ b/cq/scripts/slave/recipe/catatester_binaries_runner.py
@@ -0,0 +1,129 @@
+"""Recipe for generating and testing test binaries."""
+import os
+from typing import Dict, Union
+
+from slave import base_recipe
+from slave.step import catatester_binaries_step
+from slave.step import cipd_upload_step
+from slave.step import py_env_setup_step
+from slave.step import py_test_step
+from slave.test import test_generator
+from tools import subprocess42
+
+_CIPD_PACKAGE_ROOT = os.path.join('lab_system', 'cipd_packages')
+
+
+# (b/149845115) Current lab environment can only support the below libc version
+TARGETED_LIBC_VERSION = '2.28'
+
+BUILD_CONFIGS = {
+    'catatester_binaries': {
+        'py3': False,
+        'requirements_file_name': 'requirements.txt',
+    },
+    'py3_catatester_binaries': {
+        'py3': True,
+        'requirements_file_name': 'requirements3.txt',
+    },
+}
+
+Config = Dict[str, Union[bool, str]]
+
+
+def GetValidBuildNames():
+  return list(BUILD_CONFIGS.keys())
+
+
+def CreateRecipe(build_name: str, eureka_root: str, **kwargs):
+  if build_name in BUILD_CONFIGS:
+    return CatatesterBinariesRecipe(
+        config=BUILD_CONFIGS[build_name], eureka_root=eureka_root, **kwargs)
+  raise base_recipe.BuildNameNotSupportedError(build_name)
+
+
+class CatatesterBinariesRecipe(base_recipe.BaseRecipe):
+  """Recipe to generate and test Catatester binaries."""
+
+  def __init__(self, config: Config, eureka_root: str, **kwargs):
+    base_recipe.BaseRecipe.__init__(self, eureka_root=eureka_root, **kwargs)
+    self._py3 = config.get('py3', False)
+    self._requirements_file_name = config.get('requirements_file_name',
+                                              'requirements.txt')
+
+  # pylint: disable=method-hidden
+  def _supported_libc_found(self):
+    """Return True if supported version is found; otherwise, False."""
+    short_timeout_secs = 5
+    cmd = ['ldd', '--version']
+    output, _, returncode, _ = subprocess42.call_with_timeout(
+        cmd, timeout=short_timeout_secs, text=True)
+    if returncode != 0:
+      return False
+    return TARGETED_LIBC_VERSION in output
+
+  def get_steps(self):
+    requirements_project = 'test'
+    requirements_path = os.path.join('lab_system', self._requirements_file_name)
+    catatester_python_packages = 'team-catatester/python_packages'
+
+    test_regex = '.*test_binaries.py$'
+    test_timeout = 60 * 15  # 15 minutes
+    additional_import_paths = [
+        self._eureka_root / path for path in (
+            'builder/masters',
+            'builder/masters/src',
+            'chromium/src/third_party/catapult/telemetry',
+            'chromium/src/chromecast/internal',
+            'chromium/src/chromecast/tools',
+            'continuous-tests',
+            'continuous-tests/cq/scripts',
+            'test',
+            'testing/external/autotest',
+            'testing/external/chromite',
+            'testing/sprockets',
+        )
+    ]
+    steps = [
+        py_env_setup_step.PyEnvSetupStep(
+            additional_import_paths=additional_import_paths,
+            requirements_project=requirements_project,
+            requirements_path=requirements_path,
+            local_packages_dir=catatester_python_packages,
+            py3=self._py3,
+            **self._step_kwargs),
+        catatester_binaries_step.CatatesterBinariesBuildStep(
+            halt_on_failure=True, py3=self._py3, **self._step_kwargs),
+        py_test_step.PyTestStep(
+            test_file_filter_regex=test_regex,
+            timeout=test_timeout,
+            project='test',
+            halt_on_failure=True,
+            **self._step_kwargs),
+    ]
+
+    if self._supported_libc_found():
+      tool_step = self._GetCIPDUploadStep(
+          os.path.join(_CIPD_PACKAGE_ROOT, 'tool_binaries.yaml'),
+          'Upload tool binaries')
+      test_step = self._GetCIPDUploadStep(
+          os.path.join(_CIPD_PACKAGE_ROOT, 'test_binaries.yaml'),
+          'Upload test binaries',
+          verification_timeout='30m0s')
+      steps.extend([tool_step, test_step])
+
+    # Temporary single package upload. See b/118714546.
+    standalone_step = self._GetCIPDUploadStep(
+        os.path.join(_CIPD_PACKAGE_ROOT, 'standalone_binary.yaml'),
+        'Upload standalone binary')
+    steps.append(standalone_step)
+
+    return steps
+
+  def _GetCIPDUploadStep(self, pkg_def, step_name, verification_timeout=None):
+    """Helper to generate a CIPDUploadStep."""
+    return cipd_upload_step.CIPDUploadStep(
+        os.path.join('test', pkg_def),
+        verification_timeout=verification_timeout,
+        step_name=step_name,
+        **self._step_kwargs)
+
diff --git a/cq/scripts/slave/recipe/ccstudio_builder.py b/cq/scripts/slave/recipe/ccstudio_builder.py
new file mode 100644
index 0000000..d532e75
--- /dev/null
+++ b/cq/scripts/slave/recipe/ccstudio_builder.py
@@ -0,0 +1,45 @@
+"""Recipe for running a CCStudio build on the given device."""
+import os
+
+from slave import base_recipe
+from slave.step import ccstudio_step
+from slave.step import py_env_setup_step
+
+
+
+def GetValidBuildNames():
+  """Get all valid build names."""
+  return ['abbey-eng', 'abbey-user']
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Build a recipe object."""
+  return CCStudioRecipe(build_name, **kwargs)
+
+
+class CCStudioRecipe(base_recipe.BaseRecipe):
+  """Recipe to run CCStudio build."""
+
+  def __init__(self, build_name, **kwargs):
+    prop = kwargs.get('properties', {})
+    prop['skip_gclient'] = True
+    if 'properties' not in kwargs:
+      kwargs['properties'] = prop
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+    self._build_name = build_name
+
+  def get_steps(self):
+    requirements_project = 'abbey'
+    requirements_path = os.path.join('build', 'requirements.txt')
+    abbey_python_packages = 'team-abbey/python_packages'
+    return [
+        py_env_setup_step.PyEnvSetupStep(
+            local_packages_dir=abbey_python_packages,
+            py3=True,
+            requirements_path=requirements_path,
+            requirements_project=requirements_project,
+            **self._step_kwargs),
+        ccstudio_step.CCStudioStep(self._build_name,
+                                   **self._step_kwargs),
+    ]
+
diff --git a/cq/scripts/slave/recipe/cipd_package_upload.py b/cq/scripts/slave/recipe/cipd_package_upload.py
new file mode 100644
index 0000000..b62fefc
--- /dev/null
+++ b/cq/scripts/slave/recipe/cipd_package_upload.py
@@ -0,0 +1,84 @@
+"""Recipe for uploading test source code to CIPD."""
+
+from __future__ import absolute_import
+import os
+
+from slave import base_recipe
+from slave.step import cipd_upload_step
+
+
+# _BUILD_CONFIG entries are keyed by build name. Config dicts must include
+# 'pkg_project' and 'pkg_def'. If version is supplied and True, then a version
+# tag will be added with the format 'version:<build-number>'.
+_BUILD_CONFIG = {
+    'catatester_devicemap_upload': {
+        'pkg_project': 'continuous-tests',
+        'pkg_def': 'catatester/devicemap/cipd.yaml',
+        'refs': ['live', 'staging'],
+    },
+    'catatester_puppet_upload': {
+        'pkg_project': 'continuous-tests',
+        'pkg_def': 'catatester/puppet_cipd_pkg.yaml',
+        'refs': ['staging'],
+    },
+    'continuous_tests_files_upload': {
+        'pkg_project': 'continuous-tests',
+        'pkg_def': 'essential_pkg.yaml',
+        'refs': ['live', 'staging'],
+    },
+}
+
+
+def GetValidBuildNames():
+  return list(_BUILD_CONFIG.keys())
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Creates a recipe based on build info stored in _BUILD_CONFIG.
+
+  Args:
+    build_name: The name of the build.
+
+  Raises:
+    ValueError if required info isn't included in config.
+  """
+  build_cfg = _BUILD_CONFIG[build_name]
+  return CIPDPackageUploadRecipe(
+      build_cfg['pkg_project'],
+      build_cfg['pkg_def'],
+      refs=build_cfg.get('refs'),
+      include_version=build_cfg.get('version'),
+      **kwargs)
+
+
+class CIPDPackageUploadRecipe(base_recipe.BaseRecipe):
+  """Uploads the designated test source files to CIPD."""
+
+  def __init__(self, pkg_project, pkg_def, refs=None, tags=None,
+               include_version=False, **kwargs):
+    """Constructor.
+
+    Args:
+      pkg_project: The project containing the pkg_def file.
+      pkg_def: The relative path to the pkg_def file, from the project root.
+      refs: A list of refs to be assigned to the uploaded cipd package.
+      tags: A dict of tags to be assigned to the uploaded cipd package.
+    """
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+    self._pkg_project = pkg_project
+    self._pkg_def = pkg_def
+    self._refs = refs
+    self._tags = tags or {}
+
+    if include_version:
+      self._tags['version'] = self.build_number
+
+  def get_steps(self):
+    return [
+        cipd_upload_step.CIPDUploadStep(
+            os.path.join(self._pkg_project, self._pkg_def),
+            refs=self._refs,
+            tags=self._tags,
+            **self._step_kwargs)]
+
+
diff --git a/cq/scripts/slave/recipe/clang_builder.py b/cq/scripts/slave/recipe/clang_builder.py
new file mode 100644
index 0000000..542367a
--- /dev/null
+++ b/cq/scripts/slave/recipe/clang_builder.py
@@ -0,0 +1,656 @@
+"""Recipe for running a clang-based cast_shell build on the given patch."""
+import pathlib
+
+from cgi import test
+from slave import base_recipe
+from slave import base_step
+from slave.step import cast_shell_step
+from slave.step import code_coverage_step
+from slave.step import landmine_step
+from slave.step import make_clean_step
+from slave.step import unittest_step
+from slave.test import test_generator
+
+BUILD_CONFIGS = {
+    # TODO(b/73231972) remove after migration to atv_arm_eng
+    'arm_androidtv_debug': {
+        'build_branding': 'chrome',
+        'build_args_product': 'atv_arm_eng',
+        'build_args_official': False,
+        'build_product': 'atv',
+        'build_flavor': 'Debug',
+        'build_arch': 'arm',
+        'run_gtests': False,
+    },
+    'asan_x64_eng': {
+        'build_args_product': 'asan_x64_eng',
+        'build_args_official': False,
+        # TODO(b/148885147) Remove params below
+        'build_branding': 'chrome',
+        'build_product': 'chromecast',
+        'build_flavor': 'Eng',
+        'build_arch': 'clang',
+        'sanitizer': 'asan',
+    },
+    # Replacement of content_shell_androidtv_arm64_debug_gn
+    'atv_arm64_eng': {
+        'build_args_product': 'atv_arm64_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+    },
+    'atv_arm64_emulator_eng': {
+        'build_args_product': 'atv_arm64_emulator_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+    },
+    # Replacement of content_shell_androidtv_arm64_internal_gn
+    'atv_arm64_user': {
+        'build_args_product': 'atv_arm64_user',
+        'build_args_official': True,
+        'run_gtests': False,
+    },
+    # Replacement of content_shell_androidtv_arm_debug_gn, arm_androidtv_debug
+    'atv_arm_eng': {
+        'build_args_product': 'atv_arm_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+    },
+    # Replacement of content_shell_androidtv_arm_internal_gn
+    'atv_arm_user': {
+        'build_args_product': 'atv_arm_user',
+        'build_args_official': True,
+        'run_gtests': False,
+    },
+    # Replacement of x86_androidtv_shell
+    'atv_x86_emulator_eng': {
+        'build_args_product': 'atv_x86_emulator_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+    },
+    # Replacement of content_shell_androidtv_x86_debug_gn
+    'atv_x86_eng': {
+        'build_args_product': 'atv_x86_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+    },
+    # Replacement of content_shell_androidtv_x86_internal_gn
+    'atv_x86_user': {
+        'build_args_product': 'atv_x86_user',
+        'build_args_official': True,
+        'run_gtests': False,
+    },
+    'atv_x64_emulator_eng': {
+        'build_args_product': 'atv_x64_emulator_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+    },
+    'atv_x64_eng': {
+        'build_args_product': 'atv_x64_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+    },
+    'atv_x64_user': {
+        'build_args_product': 'atv_x64_user',
+        'build_args_official': True,
+        'run_gtests': False,
+    },
+    # Replacement of content_shell_grte_audio
+    'audio_grte_eng': {
+        'build_args_product': 'audio_grte_eng',
+        'build_args_official': False,
+        'build_gtests': False,
+    },
+    # Replacement of content_shell_clang_audio_internal_gn
+    'audio_x64_eng': {
+        'build_args_product': 'audio_x64_eng',
+        'build_args_official': False,
+        'timeout_secs_run_gtest': 1 * 60 * 60,  # Limit each test step by 1h
+        # TODO(b/148885147) Remove params below
+        'build_branding': 'chrome',
+        'build_product': 'audio',
+        'build_flavor': 'Debug',
+        'build_arch': 'clang',
+    },
+    # Replacement of content_shell_clang_audio_gn
+    'audio_x64_user': {
+        'build_args_product': 'audio_x64_user',
+        'build_args_official': False,
+        'timeout_secs_run_gtest': 1 * 60 * 60,  # Limit each test step by 1h
+        # TODO(b/148885147) Remove params below
+        'build_branding': 'chromium',
+        'build_product': 'audio',
+        'build_flavor': 'Debug',
+        'build_arch': 'clang',
+    },
+    # Replacement of content_shell_grte_chirp
+    'audioassistant_grte_eng': {
+        'build_args_product': 'audioassistant_grte_eng',
+        'build_args_official': False,
+        'build_gtests': False,
+    },
+    # Replacement of content_shell_x86_chirp, x86_clang_audio_chirp
+    'audioassistant_x64_eng': {
+        'build_args_product': 'audioassistant_x64_eng',
+        'build_args_official': False,
+        'timeout_secs_run_gtest': 1 * 60 * 60,  # Limit each test step by 1h
+        # TODO(b/145840052): Remove once the build is fast enough.
+        'timeout_secs_build_gtests': 5 * 60 * 60,
+        # TODO(b/148885147) Remove params below
+        'build_branding': 'chrome',
+        'build_product': 'audio',
+        'build_flavor': 'Debug',
+        'build_arch': 'clang',
+        'enable_assistant': True,
+    },
+    # Replacement of content_shell_cube_arm_debug, arm_cube_debug
+    'cube_arm_eng': {
+        'build_args_product': 'cube_arm_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+    },
+    # Replacement of content_shell_cube_arm_internal, arm_cube_internal
+    'cube_arm_user': {
+        'build_args_product': 'cube_arm_user',
+        'build_args_official': True,
+        'run_gtests': False,
+    },
+    # Replacement of content_shell_cube_x86_debug, x86_cube_debug
+    'cube_x64_eng': {
+        'build_args_product': 'cube_x64_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+    },
+    # Replacement of content_shell_cube_x86_internal, x86_cube_internal
+    'cube_x64_user': {
+        'build_args_product': 'cube_x64_user',
+        'build_args_official': True,
+        'run_gtests': False,
+    },
+    # Replacement of grte_manhattan_camera
+    'displayassistant_grte_camera_eng': {
+        'build_args_product': 'displayassistant_grte_camera_eng',
+        'build_args_official': False,
+        'build_gtests': False,
+    },
+    # Replacement of content_shell_grte_manhattan
+    'displayassistant_grte_eng': {
+        'build_args_product': 'displayassistant_grte_eng',
+        'build_args_official': False,
+        'build_gtests': False,
+    },
+    # Replacement of grte_opal_camera
+    'displayassistant_grte_opal_camera_eng': {
+        'build_args_product': 'displayassistant_grte_opal_camera_eng',
+        'build_args_official': False,
+        'build_gtests': False,
+    },
+    # Replacement of grte_opal
+    'displayassistant_grte_opal_eng': {
+        'build_args_product': 'displayassistant_grte_opal_eng',
+        'build_args_official': False,
+        'build_gtests': False,
+    },
+    # Replacement of content_shell_x86_manhattan, x86_manhattan
+    'displayassistant_x64_eng': {
+        'build_args_product': 'displayassistant_x64_eng',
+        'build_args_official': False,
+        'timeout_secs_run_gtest': 1 * 60 * 60,  # Limit each test step by 1h
+        # TODO(b/145840052): Remove once the build is fast enough.
+        'timeout_secs_build_gtests': 5 * 60 * 60,
+        # TODO(b/148885147) Remove params below
+        'build_branding': 'chrome',
+        'build_product': 'display',
+        'build_flavor': 'Debug',
+        'build_arch': 'clang',
+        'enable_assistant': True,
+    },
+    'gabbro_arm_eng': {
+        'build_args_product': 'gabbro_arm_debug',
+        'build_args_official': False,
+        'run_gtests': False,
+    },
+    'gabbro_arm_user': {
+        'build_args_product': 'gabbro_arm_internal',
+        'build_args_official': True,
+        'run_gtests': False,
+    },
+    'gabbro_x64_eng': {
+        'build_args_product': 'gabbro_x86_debug',
+        'build_args_official': False,
+        'run_gtests': False,
+    },
+    'gabbro_x64_user': {
+        'build_args_product': 'gabbro_x86_internal',
+        'build_args_official': True,
+        'run_gtests': False,
+    },
+    'grte_opal_selina': {
+        'build_branding': 'chrome',
+        'build_args_product': 'grte_opal_selina',
+        'build_args_official': False,
+        'build_product': 'display',
+        'build_flavor': 'Eng',
+        'build_arch': 'x86',
+        'enable_assistant': True,
+        'build_gtests': False,
+    },
+    # Replacement of content_shell_desktop_msan, memory_sanitizer
+    'msan_x64_eng': {
+        'build_args_product': 'msan_x64_eng',
+        'build_args_official': False,
+        'timeout_secs_run_gtest': 1 * 60 * 60,  # Limit each test step by 1h
+        # TODO(b/148885147) Remove params below
+        'build_branding': 'chrome',
+        'build_product': 'chromecast',
+        'build_flavor': 'Eng',
+        'build_arch': 'clang',
+        'sanitizer': 'msan',
+    },
+    # Replacement of content_shell_soba_arm_debug_gn, arm_soba_debug
+    'soba_arm_eng': {
+        'build_args_product': 'soba_arm_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+        'check_android_license': True,
+    },
+    # Replacement of content_shell_soba_arm_noassist_debug_gn
+    'soba_arm_noassist_eng': {
+        'build_args_product': 'soba_arm_noassist_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+        'check_android_license': True,
+    },
+    # Replacement of content_shell_soba_arm_noassist_internal_gn
+    'soba_arm_noassist_user': {
+        'build_args_product': 'soba_arm_noassist_user',
+        'build_args_official': True,
+        'run_gtests': False,
+        'check_android_license': True,
+    },
+    # Replacement of content_shell_soba_arm_internal_gn
+    'soba_arm_user': {
+        'build_args_product': 'soba_arm_user',
+        'build_args_official': True,
+        'run_gtests': False,
+        'check_android_license': True,
+    },
+    # Replacement of content_shell_soba_x86_debug_gn, x86_soba_debug
+    'soba_x64_eng': {
+        'build_args_product': 'soba_x64_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+        'check_android_license': True,
+    },
+    # Replacement of thread_sanitizer
+    'tsan_x64_eng': {
+        'build_args_product': 'tsan_x64_eng',
+        'build_args_official': False,
+    },
+    # Replacement of content_shell_udon_arm_debug_gn, arm_udon_debug
+    'udon_arm_eng': {
+        'build_args_product': 'udon_arm_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+    },
+    # Replacement of content_shell_udon_arm_internal_gn
+    'udon_arm_user': {
+        'build_args_product': 'udon_arm_user',
+        'build_args_official': True,
+        'run_gtests': False,
+    },
+    # Replacement of content_shell_udon_x86_debug_gn, x86_udon_debug
+    'udon_x64_eng': {
+        'build_args_product': 'udon_x64_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+    },
+    # Replacement of content_shell_udon_x86_internal_gn
+    'udon_x64_user': {
+        'build_args_product': 'udon_x64_user',
+        'build_args_official': True,
+        'run_gtests': False,
+    },
+    # Replacement of x86_udon_assistant_debug
+    'udonassistant_x64_eng': {
+        'build_args_product': 'udonassistant_x64_eng',
+        'build_args_official': False,
+        'run_gtests': False,
+    },
+    # Replacement of content_shell_grte_video
+    'video_grte_eng': {
+        'build_args_product': 'video_grte_eng',
+        'build_args_official': False,
+        'build_gtests': False,
+    },
+    # CWR, x64, release
+    'cwr_x64-user': {
+        'build_args_product': 'video_cwr_x64_user',
+        'build_args_official': False,
+        'build_gtests': False,
+        'build_target': 'chromecast/internal/build/packaging:cwr_package_cksum',
+    },
+    # CWR, x64, debug
+    'cwr_x64-eng': {
+        'build_args_product': 'video_cwr_x64_eng',
+        'build_args_official': False,
+        'build_gtests': False,
+        'build_target': 'chromecast/internal/build/packaging:cwr_package_cksum',
+    },
+    # CWR for RaspberryPi, ARMv7a, release
+    'cwr_rpix_armv7a-user': {
+        'build_args_product': 'video_cwr_rpix_armv7a_user',
+        'build_args_official': False,
+        'build_gtests': False,
+        'build_target': 'chromecast/internal/build/packaging:cwr_package_cksum',
+        'enable_build_accelerator': False,
+    },
+    # CWR for RaspberryPi, ARMv7a, debug
+    'cwr_rpix_armv7a-eng': {
+        'build_args_product': 'video_cwr_rpix_armv7a_eng',
+        'build_args_official': False,
+        'build_gtests': False,
+        'build_target': 'chromecast/internal/build/packaging:cwr_package_cksum',
+        'enable_build_accelerator': False,
+    },
+    # CWR for WebOS, ARMv7a softfp, release
+    'cwr_webos_arm-user': {
+        'build_args_product': 'video_cwr_webos_arm_user',
+        'build_args_official': False,
+        'build_gtests': False,
+        'build_target': 'chromecast/internal/build/packaging:cwr_package_cksum',
+        'enable_build_accelerator': False,
+    },
+    # CWR for WebOS, ARMv7a softfp, debug
+    'cwr_webos_arm-eng': {
+        'build_args_product': 'video_cwr_webos_arm_eng',
+        'build_args_official': False,
+        'build_gtests': False,
+        'build_target': 'chromecast/internal/build/packaging:cwr_package_cksum',
+        'enable_build_accelerator': False,
+    },
+    # CWR for WebOS24, ARMv7a softfp, release
+    'cwr_webos24_arm-user': {
+        'build_args_product': 'video_cwr_webos24_arm_user',
+        'build_args_official': False,
+        'build_gtests': False,
+        'build_target': 'chromecast/internal/build/packaging:cwr_package_cksum',
+        'enable_build_accelerator': False,
+    },
+    # CWR for WebOS24, ARMv7a softfp, debug
+    'cwr_webos24_arm-eng': {
+        'build_args_product': 'video_cwr_webos24_arm_eng',
+        'build_args_official': False,
+        'build_gtests': False,
+        'build_target': 'chromecast/internal/build/packaging:cwr_package_cksum',
+        'enable_build_accelerator': False,
+    },
+    # CWR for CrOS, ARMv7a hard, release
+    'cwr_cros_arm-user': {
+        'build_args_product': 'video_cwr_cros_arm_user',
+        'build_args_official': False,
+        'build_gtests': False,
+        'build_target': 'chromecast/internal/build/packaging:cwr_package_cksum',
+        'enable_build_accelerator': False,
+    },
+    # CWR for CrOS, ARMv7a hard, debug
+    'cwr_cros_arm-eng': {
+        'build_args_product': 'video_cwr_cros_arm_eng',
+        'build_args_official': False,
+        'build_gtests': False,
+        'build_target': 'chromecast/internal/build/packaging:cwr_package_cksum',
+        'enable_build_accelerator': False,
+    },
+    # Replacement of x86_clang_internal_absolute_code_coverage
+    'video_x64_absolute_code_coverage_eng': {
+        'build_branding': 'chrome',
+        'build_args_product': 'video_x64_eng',
+        'build_args_official': False,
+        'build_flavor': 'Debug',  # Must be Debug for code coverage to work.
+        'build_product': 'chromecast',
+        'build_arch': 'clang',
+        'code_coverage': True,
+        'report_to_zoss': True,
+        'absolute_coverage': True,
+        'timeout_secs_build_gtests': 5 * 60 * 60,
+    },
+    # Replacement of content_shell_clang_internal_gn, x86_clang_internal
+    'video_x64_eng': {
+        'build_args_product': 'video_x64_eng',
+        'build_args_official': False,
+        'timeout_secs_run_gtest': 1 * 60 * 60,  # Limit each test step by 1h
+        # TODO(b/148885147) Remove params below
+        'build_branding': 'chrome',
+        'build_product': 'chromecast',
+        'build_flavor': 'Debug',  # Must be Debug for code coverage to work.
+        'build_arch': 'clang',
+        'code_coverage': False,  # TODO(b/33835295): Temporarily disable since
+        # code coverage is often causing builds to take
+        # an extra hour to complete and not actually
+        # producing useful coverage results.
+    },
+    # Replacement of content_shell_unfork_clang_gn
+    'video_x64_user': {
+        'build_args_product': 'video_x64_user',
+        'build_args_official': False,
+        'timeout_secs_run_gtest': 1 * 60 * 60,  # Limit each test step by 1h
+        # TODO(b/148885147) Remove params below
+        'build_branding': 'chromium',
+        'build_product': 'chromecast',
+        'build_flavor': 'Debug',  # Must be Debug for code coverage to work.
+        'build_arch': 'clang',
+        'code_coverage': False,  # TODO(b/33835295): Temporarily disable since
+        # code coverage is often causing builds to take
+        # an extra hour to complete and not actually
+        # producing useful coverage results.
+    },
+    # TODO(b/73288720) remove after migration to atv_x64_emulator_eng
+    'x86_androidtv_shell': {
+        'build_branding': 'chrome',
+        'build_args_product': 'atv_x64_emulator_eng',
+        'build_args_official': False,
+        'build_product': 'atv',
+        'build_flavor': 'Debug',
+        'build_arch': 'x86',
+        'run_gtests': False,
+    },
+    # This build is only for libassistant code coverage experiment.
+    # TODO(mengdi): make sure this build only build and run libassistant
+    # unittests with --whitelisted_tests.
+    'x86_clang_audio_chirp_libassistant_code_coverage': {
+        'build_branding': 'chrome',
+        'build_args_product': 'audioassistant_x64_eng',
+        'build_args_official': False,
+        'build_product': 'audio',
+        'build_flavor': 'Debug',
+        'build_arch': 'clang',
+        'enable_assistant': True,
+        'absolute_coverage': True,
+        'code_coverage': True,
+
+        # TODO(b/145840052): Remove once the build is fast enough.
+        'timeout_secs_build_gtests': 5 * 60 * 60,
+    },
+    # TODO(b/73288720) remove after migration to
+    # video_x64_absolute_code_coverage_eng
+    'x86_clang_internal_absolute_code_coverage': {
+        'build_branding': 'chrome',
+        'build_args_product': 'displayassistant_x64_eng',
+        'build_args_official': False,
+        'build_product': 'chromecast',
+        'build_flavor': 'Debug',
+        'build_arch': 'clang',
+        'code_coverage': True,
+        'absolute_coverage': True,
+        'enable_assistant': True,
+
+        # TODO(b/145840052): Remove once the build is fast enough.
+        'timeout_secs_build_gtests': 5 * 60 * 60,
+    },
+    # TODO(b/73231972) Not used in CQ, to be removed
+    'x86_clang_internal_incremental_code_coverage': {
+        'build_branding': 'chrome',
+        'build_args_product': 'video_x64_eng',
+        'build_args_official': False,
+        'build_product': 'chromecast',
+        'build_flavor': 'Debug',
+        'build_arch': 'clang',
+        'code_coverage': True,
+
+        # TODO(b/145840052): Remove once the build is fast enough.
+        'timeout_secs_build_gtests': 5 * 60 * 60,
+    },
+}
+
+
+def GetValidBuildNames():
+  return list(BUILD_CONFIGS.keys())
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  config = BUILD_CONFIGS[build_name]
+  # Extracts default_properties from build config and adds it to the overall
+  # properties in kwargs.
+  default_properties = config.get('default_properties', {})
+  kwargs['properties'] = dict(
+      list(default_properties.items()) +
+      list(kwargs.get('properties', {}).items()))
+  return ClangRecipe(config, **kwargs)
+
+
+class ClangRecipe(base_recipe.BaseRecipe):
+  """Recipe to run a clang-based cast-shell build."""
+
+  def __init__(self, config, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self,
+        check_android_license=config.get('check_android_license', False),
+        enable_build_accelerator=config.get('enable_build_accelerator', True),
+        **kwargs)
+    self._config = config
+    self._properties = kwargs.get('properties', {})
+
+  def get_steps(self):
+    build_branding = self._config.get('build_branding', None)
+    build_args_product = self._config['build_args_product']
+    build_product = self._config.get('build_product', None)
+    build_args_official = self._config.get('build_args_official', None)
+    build_arch = self._config.get('build_arch', None)
+    build_flavor = self._config.get('build_flavor', None)
+    build_gtests = self._config.get('build_gtests', True)
+    run_gtests = self._config.get('run_gtests', True)
+    code_coverage = self._config.get('code_coverage', False)
+    report_to_zoss = self._config.get('report_to_zoss', False)
+    absolute_coverage = self._config.get('absolute_coverage', False)
+    enable_assistant = self._config.get('enable_assistant', False)
+    grte_compatible = self._config.get('grte_compatible', False)
+    sanitizer = self._config.get('sanitizer', None)
+    build_target = self._config.get('build_target', None)
+
+    steps = []
+
+    if self.build_system == 'catabuilder':
+      steps.append(make_clean_step.MakeCleanStep(**self._step_kwargs))
+    else:
+      steps += [
+          landmine_step.LandmineStep(**self._step_kwargs),
+      ]
+
+    # Sanitizers work by running the unittests. No need to waste time
+    # building cast shell by itself.
+    # Always build on CB to have an option of manual debug
+    # out_dir generated by CastShellStep also used by RunCoverageCommandStep.
+    if sanitizer is None or self.build_system == 'catabuilder':
+      steps += [
+          cast_shell_step.CastShellStep(
+              build_branding,
+              build_arch,
+              build_product,
+              build_flavor,
+              enable_assistant,
+              build_args_official=build_args_official,
+              code_coverage=code_coverage,
+              grte_compatible=grte_compatible,
+              build_args_product=build_args_product,
+              halt_on_failure=True,
+              sanitizer=sanitizer,
+              build_target=build_target,
+              **self._step_kwargs),
+      ]
+
+    if build_gtests:
+      steps += [
+          unittest_step.BuildAffectedUnitTestsStep(
+              build_branding,
+              build_arch,
+              build_product,
+              build_flavor,
+              enable_assistant,
+              build_args_official=build_args_official,
+              sanitizer=sanitizer,
+              code_coverage=code_coverage,
+              absolute_coverage=absolute_coverage,
+              build_args_product=build_args_product,
+              halt_on_failure=True,
+              timeout_secs=self._config.get('timeout_secs_build_gtests',
+                                            base_step.STEP_MAX_TIME_SECONDS),
+              **self._step_kwargs),
+      ]
+
+    # TODO(maasen): Move issue & patchset to properties on the base_step
+    #     and generate this URL there.
+    if build_gtests and run_gtests and not code_coverage:
+      if 'issue' in self._properties and 'patchset' in self._properties:
+        report_url = 'http://go/cast-cq/coverage/{0}/{1}'.format(
+            self._properties['issue'], self._properties['patchset'])
+      else:
+        report_url = ''
+
+      steps += [
+          unittest_step.RunAffectedUnitTestsStep(
+              build_branding,
+              build_arch,
+              build_product,
+              build_flavor,
+              report_url,
+              steps,
+              sanitizer=sanitizer,
+              build_args_product=build_args_product,
+              build_args_official=build_args_official,
+              enable_network_service=True,
+              timeout_secs=self._config.get('timeout_secs_run_gtest',
+                                            base_step.STEP_MAX_TIME_SECONDS),
+              **self._step_kwargs),
+      ]
+
+    elif code_coverage:
+
+      if report_to_zoss:
+        report_format = 'lcov'
+        ignore_filename_regex = '.*/gen/.*|.*/third_party/.*'
+      else:
+        report_format = None
+        ignore_filename_regex = None
+
+      coverage_step = code_coverage_step.RunCoverageCommandStep(
+          build_branding,
+          build_arch,
+          build_product,
+          build_flavor,
+          sanitizer=sanitizer,
+          build_args_product=build_args_product,
+          build_args_official=build_args_official,
+          ignore_filename_regex=ignore_filename_regex,
+          report_format=report_format,
+          **self._step_kwargs,
+      )
+      steps.append(coverage_step)
+      if report_to_zoss:
+        steps.append(
+            code_coverage_step.CollectLcovCoverageStep(
+                lcov_files=coverage_step.get_lcov_output_file_paths(),
+                **self._step_kwargs,
+            )
+        )
+
+    return steps
diff --git a/cq/scripts/slave/recipe/clang_format.py b/cq/scripts/slave/recipe/clang_format.py
new file mode 100644
index 0000000..3275dda
--- /dev/null
+++ b/cq/scripts/slave/recipe/clang_format.py
@@ -0,0 +1,60 @@
+"""Recipe for running clang-format-diff on the given build."""
+
+from __future__ import absolute_import
+from slave import base_recipe
+from slave.step import clang_format_step
+
+
+# Note that the keys are Gerrit project names.
+PROJECT_CONFIGS = {
+    'chromecast/internal': {
+        'deny_list_filter': [r'vendor/media/google3/(.*)', 'android/prebuilt',
+        r'(.*)/third_party/(.*)']
+    },
+    'chromium/src': {
+        'deny_list_filter': [r'.*'],
+    },
+    'eureka/playready_cdm': {
+        'deny_list_filter': [r'cast_playready_dummy.cc'],
+    },
+    'libassistant/internal/third_party': {
+        'deny_list_filter': [r'.*'],
+    },
+    'vendor/amlogic': {
+        'deny_list_filter': [r'(\w+)/prebuilt/(.*)'],
+    },
+    'vendor/google/neural_network_models': {
+        'deny_list_filter': [
+            r'NBGParser',
+        ],
+    },
+    'vendor/marvell': {
+        'deny_list_filter': [r'(\w+)/prebuilt/(.*)'],
+    },
+}
+
+
+def GetValidBuildNames():
+  return ['clang_format']
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  del build_name  # Unused.
+  return ClangFormatRecipe(**kwargs)
+
+
+class ClangFormatRecipe(base_recipe.BaseRecipe):
+  """Recipe to clang-format a change."""
+
+  def get_steps(self):
+    allow_list_filter = PROJECT_CONFIGS.get(
+        self._properties['patch_project'], {}).get('allow_list_filter', None)
+    deny_list_filter = PROJECT_CONFIGS.get(
+        self._properties['patch_project'], {}).get('deny_list_filter', None)
+
+    return [
+        clang_format_step.ClangFormatStep(allow_list_filter=allow_list_filter,
+                                          deny_list_filter=deny_list_filter,
+                                          **self._step_kwargs),
+    ]
diff --git a/cq/scripts/slave/recipe/clang_tidy.py b/cq/scripts/slave/recipe/clang_tidy.py
new file mode 100644
index 0000000..48b7e6a
--- /dev/null
+++ b/cq/scripts/slave/recipe/clang_tidy.py
@@ -0,0 +1,133 @@
+"""Recipe for running clang-tidy-diff on the given build."""
+import logging
+from slave import base_recipe
+from slave import base_step
+from slave.step import clang_tidy_step
+from slave.step import compdb_gen_step
+from slave.step import cast_shell_step
+from slave.step import unittest_step
+from slave.recipe import clang_builder
+from slave.recipe import ota_builder
+from slave.step import ota_resign_step
+
+# Note that the keys are Gerrit project names.
+# TODO(mingcl): Allow configuring build-specific filters to avoid reporting
+# duplicate clang-tidy suggestions from different builds.
+PROJECT_CONFIGS = {
+    'chromecast/internal': {
+        'deny_list_filter': [
+            r'vendor/media/google3/(.*)', 'android/prebuilt',
+            r'(.*)/third_party/(.*)'
+        ]
+    },
+}
+
+BUILD_CONFIGS = {
+    'audioassistant_x64_eng_rbe_clang_tidy': {
+        'build_args_product': 'audioassistant_x64_eng',
+        'build_args_official': False,
+        # TODO(b/145840052): Remove once the build is fast enough.
+        'timeout_secs_build_gtests': 5 * 60 * 60,
+        # TODO(b/148885147) Remove params below
+        'build_branding': 'chrome',
+        'build_product': 'audio',
+        'build_flavor': 'Debug',
+        'build_arch': 'clang',
+        'enable_assistant': True,
+        'run_gtests': False,
+    },
+    'nq-eng_clang_tidy': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.NQ_RESIGN,
+        'factory': ota_resign_step.NQ_FACTORY,
+        'usbstick': ota_resign_step.NQ_USBSTICK,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+    },
+}
+
+# The name of build directory used by actual build recipe.
+# Since the build directory name could change when the build recipe/script get
+# changed, the best we can do is to update this dictionary when needed.
+# This should be fine since clang tidy check is non-blocking.
+BUILD_DIRECTORIES = {
+    'audioassistant_x64_eng_rbe_clang_tidy': 'out_clang_audio_gn/Debug/',
+    'nq-eng_clang_tidy': 'out_chromecast_nq/release/',
+}
+
+# Specifies the recipe which will be used to make sure all files required for
+# compilation is available before running clang-tidy
+BUILD_RECIPE_NAME = {
+    'audioassistant_x64_eng_rbe_clang_tidy': 'clang_builder',
+    'nq-eng_clang_tidy': 'ota_builder',
+}
+
+
+def GetValidBuildNames():
+  return [
+      'audioassistant_x64_eng_rbe_clang_tidy',
+      'nq-eng_clang_tidy',
+  ]
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  # Extracts default_properties from build config and adds it to the overall
+  # properties in kwargs.
+  config = BUILD_CONFIGS[build_name]
+  return ClangTidyRecipe(
+      build_name,
+      build_config=config,
+      build_dir=BUILD_DIRECTORIES[build_name],
+      build_recipe_name=BUILD_RECIPE_NAME[build_name],
+      **kwargs)
+
+
+def RemoveClangTidySuffix(text):
+  if text.endswith('_clang_tidy'):
+    return text[:-len('_clang_tidy')]
+  return text
+
+
+class ClangTidyRecipe(base_recipe.BaseRecipe):
+  """Recipe to clang-tidy a change."""
+
+  def __init__(self, build_name, build_config, build_dir, build_recipe_name,
+               **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._properties = kwargs.get('properties', {})
+    self._build_name = build_name
+    self._build_config = build_config
+    self._build_dir = build_dir
+    self._build_recipe_name = build_recipe_name
+    self._builder_kwargs = kwargs
+
+  def get_steps(self):
+    allow_list_filter = PROJECT_CONFIGS.get(self._properties['patch_project'],
+                                           {}).get('allow_list_filter', None)
+    deny_list_filter = PROJECT_CONFIGS.get(self._properties['patch_project'],
+                                           {}).get('deny_list_filter', None)
+
+    # Build cast shell and unittests to generate necessary header files
+    if self._build_recipe_name == 'clang_builder':
+      build_steps = clang_builder.ClangRecipe(
+          self._build_config, **self._builder_kwargs).get_steps()
+    elif self._build_recipe_name == 'ota_builder':
+      build_steps = ota_builder.OtaRecipe(
+          RemoveClangTidySuffix(self._build_name), self._build_config,
+          **self._builder_kwargs).get_steps()
+    else:
+      logging.error('Unknown build recipe name: ' + self._build_recipe_name +
+                    '. Might need to update BUILD_RECIPE_NAME.')
+      build_steps = []
+
+    return build_steps + [
+        # Generate compilation database from the build directory.
+        compdb_gen_step.CompdbGenStep(
+            build_dir=self._build_dir, **self._step_kwargs),
+        # Then clang tidy can use the compilation database to check the code.
+        clang_tidy_step.ClangTidyStep(
+            allow_list_filter=allow_list_filter,
+            deny_list_filter=deny_list_filter,
+            **self._step_kwargs),
+    ]
diff --git a/cq/scripts/slave/recipe/clusterfuzz_builder.py b/cq/scripts/slave/recipe/clusterfuzz_builder.py
new file mode 100644
index 0000000..d97832e
--- /dev/null
+++ b/cq/scripts/slave/recipe/clusterfuzz_builder.py
@@ -0,0 +1,144 @@
+"""Recipe to build and upload fuzz testing targets to clusterfuzz GCS Bucket"""
+import os
+
+from helpers import branch_utils
+from helpers import gcs_utils
+from slave import base_recipe
+from slave.step import gcs_upload_step
+from slave.step import gn_step
+from slave.step import make_clean_step
+from slave.step import ninja_step
+from slave.step import shell_step
+
+BUILD_CONFIGS = {
+    'tcp_socket_impl_fuzzer': {
+        'archive_name': 'tcp_socket_impl_fuzzer',
+        'build_args_product': 'video_x64_eng',
+        'build_target': ('chromecast/internal/smart_home/actions/'
+                         'platform/cast:tcp_socket_impl_fuzzer'),
+        'run_gtests': False,
+        'build_args': [
+            'use_libfuzzer=true',
+            'is_asan=true',
+            'is_component_build=false',
+        ],
+        'gcs_bucket': 'catabuilder-clusterfuzz-results',
+        'gcs_path': 'tcp_socket_impl_fuzzer',
+    },
+    'udp_socket_impl_fuzzer': {
+        'archive_name': 'udp_socket_impl_fuzzer',
+        'build_args_product': 'video_x64_eng',
+        'build_target': ('chromecast/internal/smart_home/actions/'
+                         'platform/cast:udp_socket_impl_fuzzer'),
+        'run_gtests': False,
+        'build_args': [
+            'use_libfuzzer=true',
+            'is_asan=true',
+            'is_component_build=false',
+        ],
+        'gcs_bucket': 'catabuilder-clusterfuzz-results',
+        'gcs_path': 'udp_socket_impl_fuzzer',
+    },
+}
+
+
+def GetValidBuildNames():
+  return list(BUILD_CONFIGS.keys())
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  config = BUILD_CONFIGS[build_name]
+  return FuzzRecipe(config, **kwargs)
+
+
+class FuzzRecipe(base_recipe.BaseRecipe):
+  """Recipe to build and upload fuzz testing targets to clusterfuzz GCS"""
+
+  def __init__(self, config, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._build_config = config
+
+  def get_num_jobs(self):  # pylint:disable=method-hidden
+    """Count of jobs to use."""
+    setup_steps = self.get_setup_steps()
+    return setup_steps[0].get_num_jobs()
+
+  def get_steps(self):
+    cwd = os.getcwd()
+    gcs_bucket = self._build_config.get('gcs_bucket')
+    gcs_path = self._build_config.get('gcs_path')
+    jobs = self.get_num_jobs()
+    build_args = self._build_config.get('build_args')
+    build_args_product = self._build_config.get('build_args_product')
+    build_target = self._build_config.get('build_target')
+    post_submit_upload = self.build_system == 'catabuilder'
+    archive_name = self._build_config.get('archive_name')
+    fuzz_archive = '{}.zip'.format(self.build_number)
+
+    # Going to build target to out directory
+    out_dir = os.path.join('out', archive_name, self.build_number)
+    steps = []
+    if self.build_system == 'catabuilder':
+      steps.append(make_clean_step.MakeCleanStep(**self._step_kwargs))
+
+    # Generate gn gen step
+    gn_gen_step = gn_step.GnGenStep(
+        build_args_product=build_args_product,
+        build_args=build_args,
+        out_dir=out_dir,
+        halt_on_failure=True,
+        cwd=cwd,
+        **self._step_kwargs)
+    steps.append(gn_gen_step)
+
+    # Generate gn args step
+    gn_args_step = gn_step.GnArgsStep(
+        out_dir=out_dir, cwd=cwd, halt_on_failure=True, **self._step_kwargs)
+    steps.append(gn_args_step)
+
+    # Generate ninja build step
+    ninja_build_step = ninja_step.NinjaStep(
+        out_dir=out_dir,
+        target=build_target,
+        cwd=cwd,
+        jobs=jobs,
+        halt_on_failure=True,
+        **self._step_kwargs)
+    steps.append(ninja_build_step)
+
+    # Zip build artifacts excluding obj file
+    zip_output_step = shell_step.ShellStep(
+        name='Zip build artifacts',
+        command=['zip', '-rq', fuzz_archive,
+                  '.', '-x', "'*obj*'"],
+        directory=os.getcwd()+'/'+out_dir,
+        **self._step_kwargs)
+
+    steps.append(zip_output_step)
+
+    # Upload Fuzz Testing build output to gcs
+    # only if recipe is running from Catabuilder.
+    if post_submit_upload:
+      # Write and upload targets.list file
+      contents = '\n'.join(
+          BUILD_CONFIGS[config]['gcs_path'] for config in BUILD_CONFIGS)
+      targets_list_file = self._executor.write_temp_file(contents)
+
+      gcs_step = gcs_upload_step.GcsUploadStep(
+          gcs_bucket,
+          'targets.list',
+          name='Clusterfuzz GCS upload for targets.list',
+          src_path=targets_list_file,
+          **self._step_kwargs)
+
+      gcs_step = gcs_upload_step.GcsUploadStep(
+          gcs_bucket,
+          gcs_path,
+          name='Clusterfuzz GCS upload for target: ' + archive_name,
+          src_path=os.getcwd()+'/'+out_dir+'/'+fuzz_archive,
+          **self._step_kwargs)
+      steps.append(gcs_step)
+
+    return steps
diff --git a/cq/scripts/slave/recipe/code_coverage_e2e_mock.py b/cq/scripts/slave/recipe/code_coverage_e2e_mock.py
new file mode 100644
index 0000000..c915487
--- /dev/null
+++ b/cq/scripts/slave/recipe/code_coverage_e2e_mock.py
@@ -0,0 +1,78 @@
+import json
+import os
+import pathlib
+import shutil
+
+from slave import base_recipe
+from slave import base_step
+from slave.step import code_coverage_step
+
+
+def GetValidBuildNames():
+  return ['incremental-coverage-e2e-mock-1', 'absolute-coverage-e2e-mock-1']
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Defines how this recipe gets built."""
+  return CodeCoverageE2EMockRecipe(build_name, **kwargs)
+
+
+class CodeCoverageE2EMockRecipe(base_recipe.BaseRecipe):
+  """Recipe to execute e2e fake build."""
+
+  def __init__(self, builder_name, data_manager, **kwargs):
+    base_recipe.BaseRecipe.__init__(self, data_manager=data_manager, **kwargs)
+    self._builder_name = builder_name
+    self._data_manager = data_manager
+
+  def get_steps(self):
+    if 'code_health_project_path' in self._properties:
+      project_path = pathlib.Path(self._properties['code_health_project_path'])
+    else:
+      project_path = pathlib.Path(self._data_manager.get_project_path(
+          self._properties['patch_project']
+      ))
+    config_file_path = project_path / 'recipe_config.json'
+    config = json.loads(config_file_path.read_text())
+    return [
+        code_coverage_step.CollectLcovCoverageStep(
+            lcov_files=config.get('input_lcov_files', []),
+            **self._step_kwargs,
+        ),
+        CodeCoverageE2EMockStep(
+            builder_name=self._builder_name,
+            output_files=config.get('output_files', []),
+            output_properties=config.get('output_properties', {}),
+            project_path=project_path,
+            **self._step_kwargs,
+        ),
+    ]
+
+
+class CodeCoverageE2EMockStep(base_step.BaseStep):
+
+  def __init__(
+      self,
+      builder_name,
+      output_files,
+      output_properties,
+      project_path,
+      **kwargs,
+  ):
+    base_step.BaseStep.__init__(self, name="code_coverage_e2e_mock", **kwargs)
+    self._builder_name = builder_name
+    self._properties = kwargs.get('properties', {})
+    self._output_files = output_files
+    self._output_properties = output_properties
+    self._project_path = project_path
+
+  def run(self):
+    for path in self._output_files:
+      path = pathlib.Path(path)
+      dest_dir = pathlib.Path(self.get_gcs_dir()) / path.parent
+      dest_dir.mkdir(exist_ok=True, parents=True)
+      shutil.copy(self._project_path / path, dest_dir)
+
+    for key, value in self._output_properties.items():
+      self.set_build_property(key, value)
+
+    return True
diff --git a/cq/scripts/slave/recipe/combined_sdk.py b/cq/scripts/slave/recipe/combined_sdk.py
new file mode 100644
index 0000000..0025791
--- /dev/null
+++ b/cq/scripts/slave/recipe/combined_sdk.py
@@ -0,0 +1,268 @@
+"""Recipe for building Combined SDK, bootloader, kernel."""
+from helpers import branch_utils
+
+from slave import base_recipe
+from slave.step import combined_sdk_clean
+from slave.step import combined_sdk_compile
+from slave.step import py_env_setup_step
+
+
+
+FAKE_WORK_DIR = 'out'
+FAKE_PREBUILT_SUBDIR = (
+    FAKE_WORK_DIR + '/vendor/{vendor}/{product}/prebuilt')
+
+OSS_SDK_DIR = '/tmp/oss'
+OSS_SDK_RELEASE_SCRIPT = './sdk/build_scripts/release_oss.sh'
+
+BUILD_LOGS_DIR = 'sdk/build_scripts/build_log/log'
+
+MARVELL_PREBUILT_FILE = 'marvell-prebuilt.tgz'
+
+MARVELL = {
+    'name': 'marvell',
+    'build_script': 'sdk/build_scripts/build_all.sh',
+    'fake_work_dir': FAKE_WORK_DIR,
+    'fake_prebuilt_dir': FAKE_PREBUILT_SUBDIR,
+    'prebuilt_file': MARVELL_PREBUILT_FILE,
+    'include_product_param': False,
+    'requirements_project': 'marvell/sdk/amp',
+    'requirements_path': 'requirements3.txt',
+    'oss_sdk_dir': OSS_SDK_DIR,
+    'oss_sdk_release_script': OSS_SDK_RELEASE_SCRIPT,
+    'build_logs_dir': BUILD_LOGS_DIR,
+    'purge_dirs': [
+        MARVELL_PREBUILT_FILE,
+        OSS_SDK_DIR,
+        FAKE_WORK_DIR,
+    ]
+}
+
+AMLOGIC_PREBUILT_FILE = 'amlogic-prebuilt.tgz'
+
+AMLOGIC = {
+    'name': 'amlogic',
+    'build_script': 'sdk/build_scripts/build_all.sh',
+    'fake_work_dir': FAKE_WORK_DIR,
+    'fake_prebuilt_dir': FAKE_PREBUILT_SUBDIR,
+    'prebuilt_file': AMLOGIC_PREBUILT_FILE,
+    'include_product_param': True,
+    'requirements_project': 'amlogic/sdk',
+    'requirements_path': 'requirements3.txt',
+    'oss_sdk_dir': OSS_SDK_DIR,
+    'oss_sdk_release_script': OSS_SDK_RELEASE_SCRIPT,
+    'build_logs_dir': BUILD_LOGS_DIR,
+    'purge_dirs': [
+        AMLOGIC_PREBUILT_FILE,
+        OSS_SDK_DIR,
+        FAKE_WORK_DIR,
+    ]
+}
+
+QUALCOMM_PREBUILT_FILE = 'qualcomm-prebuilt.tgz'
+
+QUALCOMM = {
+    'name': 'qualcomm',
+    'build_script': 'sdk/build_scripts/build_all.sh',
+    'fake_work_dir': FAKE_WORK_DIR,
+    'fake_prebuilt_dir': FAKE_PREBUILT_SUBDIR,
+    'prebuilt_file': QUALCOMM_PREBUILT_FILE,
+    'include_product_param': True,
+    'requirements_project': 'qualcomm_sirocco/sdk',
+    'requirements_path': 'requirements3.txt',
+    'oss_sdk_dir': OSS_SDK_DIR,
+    'oss_sdk_release_script': OSS_SDK_RELEASE_SCRIPT,
+    'build_logs_dir': BUILD_LOGS_DIR,
+    'purge_dirs': [
+        QUALCOMM_PREBUILT_FILE,
+        OSS_SDK_DIR,
+        FAKE_WORK_DIR,
+        'qualcomm',
+        'u-boot',
+    ]
+}
+
+MEDIATEK_PREBUILT_FILE = 'mediatek-prebuilt.tgz'
+MEDIATEK_WORK_DIR = 'out_sdk'
+MEDIATEK = {
+    'name': 'mediatek',
+    'build_script': 'sdk/build_scripts/build_all.sh',
+    'fake_work_dir': MEDIATEK_WORK_DIR,
+    'fake_prebuilt_dir': FAKE_PREBUILT_SUBDIR,
+    'prebuilt_file': MEDIATEK_PREBUILT_FILE,
+    'include_product_param': True,
+    'requirements_project': 'kirkwood/alps/device/google/kirkwood',
+    'requirements_path': 'sdk/requirements.txt',
+    'oss_sdk_dir': OSS_SDK_DIR,
+    'oss_sdk_release_script': OSS_SDK_RELEASE_SCRIPT,
+    'build_logs_dir': BUILD_LOGS_DIR,
+    'purge_dirs': [
+        MEDIATEK_PREBUILT_FILE,
+        OSS_SDK_DIR,
+        MEDIATEK_WORK_DIR,
+        'out',
+    ]
+}
+
+BUILD_CONFIGS = {
+    'combined-sdk-kernel-bootloader': {
+        'vendor_config': MARVELL,
+        'product': 'salami',
+    },
+    'combined-sdk-kernel-bootloader-amlogic-biggie': {
+        'vendor_config': AMLOGIC,
+        'product': 'biggie',
+    },
+    'combined-sdk-kernel-bootloader-amlogic-boreal': {
+        'vendor_config': AMLOGIC,
+        'product': 'boreal',
+    },
+    'combined-sdk-kernel-bootloader-amlogic-elaine': {
+        'vendor_config': AMLOGIC,
+        'product': 'elaine',
+    },
+    'combined-sdk-kernel-bootloader-amlogic-estelle': {
+        'vendor_config': AMLOGIC,
+        'product': 'estelle',
+    },
+    'combined-sdk-kernel-bootloader-amlogic-gq': {
+        'vendor_config': AMLOGIC,
+        'product': 'gq',
+    },
+    'combined-sdk-kernel-bootloader-amlogic-korlan': {
+        'vendor_config': AMLOGIC,
+        'product': 'korlan',
+    },
+    'combined-sdk-kernel-bootloader-amlogic-lumia': {
+        'vendor_config': AMLOGIC,
+        'product': 'lumia',
+    },
+    'combined-sdk-kernel-bootloader-amlogic-newman': {
+        'vendor_config': AMLOGIC,
+        'product': 'newman',
+    },
+    'combined-sdk-kernel-bootloader-amlogic-nq': {
+        'vendor_config': AMLOGIC,
+        'product': 'nq',
+    },
+    'combined-sdk-kernel-bootloader-amlogic-sabrina': {
+        'vendor_config': AMLOGIC,
+        'product': 'sabrina',
+    },
+    'combined-sdk-kernel-bootloader-amlogic-spencer': {
+        'vendor_config': AMLOGIC,
+        'product': 'spencer',
+    },
+    'combined-sdk-kernel-bootloader-amlogic-a049': {
+        'vendor_config': AMLOGIC,
+        'product': 'a049',
+    },
+    'combined-sdk-kernel-bootloader-amlogic-venus': {
+        'vendor_config': AMLOGIC,
+        'product': 'venus',
+    },
+    'combined-sdk-kernel-bootloader-qualcomm-brezza': {
+        'vendor_config': QUALCOMM,
+        'product': 'brezza',
+    },
+    'combined-sdk-kernel-bootloader-qualcomm-sirocco': {
+        'vendor_config': QUALCOMM,
+        'product': 'sirocco',
+    },
+    'combined-sdk-kernel-bootloader-mediatek-kirkwood': {
+        'vendor_config': MEDIATEK,
+        'product': 'kirkwood',
+    },
+    'combined-sdk-kernel-bootloader-mediatek-kirkwood-user': {
+        'vendor_config': MEDIATEK,
+        'product': 'kirkwood-user',
+    },
+
+}
+
+
+def GetValidBuildNames():
+  return list(BUILD_CONFIGS.keys())
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  return CombinedSdkRecipe(build_name, BUILD_CONFIGS[build_name], **kwargs)
+
+
+class CombinedSdkRecipe(base_recipe.BaseRecipe):
+  """Recipe to run an ota build."""
+
+  def __init__(self, build_name, build_config, **kwargs):
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+    self._build_name = build_name
+    self._build_config = build_config
+
+  def _oss_run_condition(self):
+    if self.build_system != 'catabuilder':
+      return False
+    if branch_utils.is_release_branch(self._manifest_branch):
+      return True
+    return False
+
+  def get_setup_steps(self):
+    steps = []
+    purge_dirs = self._build_config['vendor_config']['purge_dirs']
+    steps.append(combined_sdk_clean.CleanPrebuiltsSDKStep(
+        purge_dirs, **self._step_kwargs))
+    steps += super(CombinedSdkRecipe, self).get_setup_steps()
+    return steps
+
+  def get_steps(self):
+    vendor_config = self._build_config['vendor_config']
+    sdk_python_packages = 'team-sdk/python_packages'
+    steps = [
+        combined_sdk_clean.DirtyCheckSDKStep(
+            halt_on_failure=False, **self._step_kwargs),
+        combined_sdk_clean.CleanCheckoutSDKStep(**self._step_kwargs),
+        combined_sdk_clean.DirtyCheckSDKStep(
+            halt_on_failure=True, **self._step_kwargs),
+        py_env_setup_step.PyEnvSetupStep(
+            local_packages_dir=sdk_python_packages,
+            py3=True,
+            requirements_path=vendor_config['requirements_path'],
+            requirements_project=vendor_config['requirements_project'],
+            **self._step_kwargs),
+        combined_sdk_compile.CompileSDKStep(
+            self._build_config, **self._step_kwargs),
+        combined_sdk_compile.CreateTarStep(
+            self._build_config, **self._step_kwargs),
+    ]
+    if self._oss_run_condition():
+      steps.append(combined_sdk_compile.OssCreateTarStep(
+          self._build_config, **self._step_kwargs))
+    steps += [
+        combined_sdk_compile.CreateLogDirStep(
+            self._build_config, **self._step_kwargs),
+        combined_sdk_compile.ArchiveArtifactsStep(
+            self._build_config, **self._step_kwargs),
+    ]
+    return steps
+
+  def get_steps_for_ota_builder(self):
+    vendor_config = self._build_config['vendor_config']
+    sdk_python_packages = 'team-sdk/python_packages'
+    return [
+        py_env_setup_step.PyEnvSetupStep(
+            local_packages_dir=sdk_python_packages,
+            py3=True,
+            requirements_path=vendor_config['requirements_path'],
+            requirements_project=vendor_config['requirements_project'],
+            **self._step_kwargs),
+        combined_sdk_compile.CompileSDKStep(
+            self._build_config, **self._step_kwargs),
+        combined_sdk_compile.CopySDKStep(self._build_config,
+                                         **self._step_kwargs),
+        combined_sdk_compile.CreateTarStep(
+            self._build_config, **self._step_kwargs),
+        combined_sdk_compile.CreateLogDirStep(
+            self._build_config, **self._step_kwargs),
+        combined_sdk_compile.ArchiveArtifactsStep(
+            self._build_config, **self._step_kwargs),
+    ]
+
diff --git a/cq/scripts/slave/recipe/config_validator.py b/cq/scripts/slave/recipe/config_validator.py
new file mode 100644
index 0000000..a09bdc1
--- /dev/null
+++ b/cq/scripts/slave/recipe/config_validator.py
@@ -0,0 +1,39 @@
+"""Recipe for running config validation checks on the given build."""
+from slave import base_recipe
+from slave.step import json_check_step
+from slave.step import xml_check_step
+
+
+PROJECT_CONFIGS = {
+  'chromium/src': {
+      'deny_list_filter': ['.*'],
+  }
+}
+
+
+def GetValidBuildNames():
+  return ['config_validator']
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  del build_name  # Unused.
+  return ConfigValidatorRecipe(**kwargs)
+
+
+class ConfigValidatorRecipe(base_recipe.BaseRecipe):
+  """Recipe to run config validatoins on a repository."""
+
+  def __init__(self, **kwargs):
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+    self._config = PROJECT_CONFIGS.get(
+        self._properties.get('patch_project'), {})
+    self._deny_list_filter = self._config.get('deny_list_filter')
+
+  def get_steps(self):
+    return [
+        json_check_step.JsonCheckStep(deny_list_filter=self._deny_list_filter,
+                                      **self._step_kwargs),
+        xml_check_step.XmlCheckStep(deny_list_filter=self._deny_list_filter,
+                                    **self._step_kwargs),
+    ]
diff --git a/cq/scripts/slave/recipe/container_autoroller_e2e_test.py b/cq/scripts/slave/recipe/container_autoroller_e2e_test.py
new file mode 100644
index 0000000..641c946
--- /dev/null
+++ b/cq/scripts/slave/recipe/container_autoroller_e2e_test.py
@@ -0,0 +1,42 @@
+import logging
+import json
+import time
+
+from helpers import git_utils
+from slave import base_recipe
+from slave import base_step
+
+
+
+def GetValidBuildNames():
+  return ['container_autoroller_e2e_test']
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Defines how this recipe gets built."""
+  return ContainterAutorollerE2ERecipe(build_name, **kwargs)
+
+
+class ContainterAutorollerE2ERecipe(base_recipe.BaseRecipe):
+  """Recipe to execute e2e fake build."""
+
+  def __init__(self, builder_name, **kwargs):
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+    self._builder_name = builder_name
+
+  def get_steps(self):
+    return [
+        ContainterAutorollerE2EStep(
+          builder_name=self._builder_name,
+          **self._step_kwargs
+        ),
+    ]
+
+
+class ContainterAutorollerE2EStep(base_step.BaseStep):
+
+  def __init__(self, builder_name, **kwargs):
+    base_step.BaseStep.__init__(self, name="e2e_mock", **kwargs)
+    self._builder_name = builder_name
+
+  def run(self):
+    return True
diff --git a/cq/scripts/slave/recipe/container_builder.py b/cq/scripts/slave/recipe/container_builder.py
new file mode 100644
index 0000000..7657cf8
--- /dev/null
+++ b/cq/scripts/slave/recipe/container_builder.py
@@ -0,0 +1,85 @@
+import os
+import pathlib
+
+from slave import base_recipe
+from slave.step import shell_step
+
+
+def GetValidBuildNames():
+  return [
+      'abbey_container',
+      'container_building_container',
+      'gotham_container',
+      'monotainer',
+      'recipe_bullseye',
+  ]
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  del build_name
+  return ContainerBuilderRecipe(**kwargs)
+
+
+class ContainerBuilderRecipe(base_recipe.BaseRecipe):
+  """Recipe to build and optionally push a container image."""
+
+  def get_steps(self):
+    steps = [ContainerBuildStep(
+        directory=str(self._eureka_root),
+        halt_on_failure=True,
+        **self._step_kwargs)]
+    if 'container_push_url' in self._properties:
+      steps += [ContainerPushStep(halt_on_failure=True, **self._step_kwargs)]
+    return steps
+
+
+class ContainerBuildStep(shell_step.ShellStep):
+
+  def __init__(self,  **kwargs):
+    super().__init__(name='container build', **kwargs)
+    self._git_config_path = pathlib.Path.home() / 'containers' / 'gitconfig'
+    self._pip_config_path = pathlib.Path.home() / 'containers' / 'pip.conf'
+    self._gcs_mirror_path = pathlib.Path.home() / 'gcs-mirror'
+    self._git_creds_path = pathlib.Path.home() / '.git-credential-cache'
+
+  def get_commands(self):
+    build_id = self.get_property('basil_build_id')
+    creds_path = self._git_creds_path
+    return [
+        self.ShellCommand([
+            'podman',
+            'build',
+            '--format=oci',
+            '--arch=amd64',
+            f'--tag={build_id}',
+            '--no-cache',
+            '--isolation=chroot',
+            f'--label=build_id={build_id}',
+            '--volume=/etc/gitconfig:/etc/gitconfig:ro',
+            '--volume=/etc/pip.conf:/etc/pip.conf:ro',
+            '--volume=/workspace/gcs-mirror:/workspace/gcs-mirror:ro',
+            ('--volume=/workspace/git-credential-cache:'
+              '/workspace/git-credential-cache:ro'),
+            self.get_property('container_context_path'),
+        ]),
+    ]
+
+
+class ContainerPushStep(shell_step.ShellStep):
+
+  def __init__(self,  **kwargs):
+    super().__init__(name='container push', **kwargs)
+
+  def get_commands(self):
+    build_id = self.get_property('basil_build_id')
+    push_url = self.get_property('container_push_url')
+    image_tag = self.get_property('buildset')
+    builder_name = self.get_property('buildername')
+    return [
+        self.ShellCommand([
+            'podman',
+            'push',
+            f'localhost/{build_id}:latest',
+            f'{push_url}/{builder_name}:{image_tag}',
+        ], num_retries=3),
+    ]
diff --git a/cq/scripts/slave/recipe/cq_e2e_mock.py b/cq/scripts/slave/recipe/cq_e2e_mock.py
new file mode 100644
index 0000000..b4162d4
--- /dev/null
+++ b/cq/scripts/slave/recipe/cq_e2e_mock.py
@@ -0,0 +1,115 @@
+import logging
+import json
+
+from helpers import git_utils
+from slave import base_recipe
+from slave import base_step
+
+
+CONFIG_TAG = 'cq_e2e_config:'
+PASS = 'PASSES'
+FAIL = 'FAILS'
+_DEFAULT_CONFIG = {}
+
+
+def GetValidBuildNames():
+  return [
+      'cq-e2e-mock-1',
+      'cq-e2e-mock-2',
+      'cq-e2e-mock-experimental',
+      'cq-e2e-mock-legacy',
+      'cq-e2e-mock-recipe',
+  ]
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Defines how this recipe gets built."""
+  return CqE2EMockRecipe(build_name, **kwargs)
+
+
+class CqE2EMockRecipe(base_recipe.BaseRecipe):
+  """Recipe to execute e2e fake build."""
+
+  def __init__(self, builder_name, **kwargs):
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+    self._builder_name = builder_name
+
+  def get_steps(self):
+    return [
+        CqE2EMockStep(builder_name=self._builder_name, **self._step_kwargs),
+    ]
+
+
+class CqE2EMockStep(base_step.BaseStep):
+
+  def __init__(self, builder_name, **kwargs):
+    base_step.BaseStep.__init__(self, name="e2e_mock", **kwargs)
+    self._builder_name = builder_name
+
+  def run(self):
+    for directory in self._get_patch_dirs():
+      build = self._get_build_config(directory)
+      if build:
+        break
+    else:
+      return True
+
+    if build.get('print_patch_order', False):
+      self._print_patch_order()
+
+    if build.get('review'):
+      self.add_review(build['review'])
+
+    outcome = build.get('outcome', PASS)
+    if outcome == PASS:
+      return True
+    if outcome == FAIL:
+      return False
+    raise ValueError('status must be set in the build config.')
+
+  def _get_build_config(self, directory):
+    logging.info('Checking for config from latest commit in %s', directory)
+    commit_message = git_utils.commit_message(self, directory)
+    lines = commit_message.split('\n')
+    build_config = None
+    for line in lines:
+      if line.startswith(CONFIG_TAG):
+        build_config = line
+    if build_config:
+      build_config = build_config[len(CONFIG_TAG):]
+      build_config = build_config.strip()
+      build_config = json.loads(build_config)
+      logging.info('Build config is %s', build_config)
+      return build_config.get(self._builder_name, _DEFAULT_CONFIG)
+    return _DEFAULT_CONFIG
+
+  def _get_patch_dirs(self):
+    dirs = list()
+    for patch in [{'project': self.patch_project}] + self.depends_on_list:
+      try:
+        new_dir = self.get_project_path(patch['project'])
+        if new_dir not in dirs:
+          dirs.append(new_dir)
+      except KeyError:
+        logging.info(
+          'Ignoring KeyError since %s is not in the manifest',
+          patch['project']
+        )
+    logging.info('patch dirs are: %s', dirs)
+    return dirs
+
+  def _print_patch_order(self):
+    main_patch = {
+        'change_number': self.get_property('issue', 0),
+        'gob_host': self.get_property('gerrit'),
+        'patchset_number': self.patchset,
+        'project': self.patch_project,
+    }
+    patches = self.depends_on_list
+    patches.append(main_patch)
+    msg = 'Patch Order: ' + ', '.join([
+        '{}/{}/{}'.format(
+            p['gob_host'], int(p['change_number']), int(p['patchset_number']))
+        for p in patches
+    ])
+    self.add_review({'message': msg})
diff --git a/cq/scripts/slave/recipe/device_unittest_runner.py b/cq/scripts/slave/recipe/device_unittest_runner.py
new file mode 100644
index 0000000..8c17a49
--- /dev/null
+++ b/cq/scripts/slave/recipe/device_unittest_runner.py
@@ -0,0 +1,170 @@
+"""Recipe for running unit test on a given deivce.
+
+The unit test running on a device involves:
+1. Download the OTA zip file
+2. Install the OTA
+3. Copy test dependencies to the device
+4. Run unit test cases
+"""
+import os
+import shlex
+import sys
+
+from slave import base_recipe
+from slave.step import device_unit_test_step
+from slave.step import fetch_ota_step
+
+
+def GetValidBuildNames():
+  return ['device_unittest']
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  del build_name  # Unused.
+  property_dict = kwargs['properties']
+  mandatory_field_list = ['product_name',
+                          'branch_name',
+                          'build_number',
+                          'build_flavor',
+                          'board_type',
+                          'device_id',
+                          'repo_alias']
+  assert all(mandatory_field in property_dict
+             for mandatory_field in mandatory_field_list)
+
+  return DeviceUnittestRecipe(**kwargs)
+
+
+class DeviceUnittestRecipe(base_recipe.BaseRecipe):
+  """Recipe to run unit tests on a device."""
+
+  def __init__(self, **kwargs):
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+    self._property_dict = kwargs['properties']
+    self._work_dir = kwargs['properties']['recipe_tmp_dir']
+
+  def get_tmp_dir(self):
+    return self._step_kwargs['executor'].get_tmp_dir()
+
+  def get_python_path_dict(self):
+    """Returns PYTHONPATH used in the unit test."""
+    python_path_list = sys.path[:]
+    for py_path in ('test', 'continuous-tests/build'):
+      python_path_list.append(os.path.join(self._work_dir, py_path))
+    return {'PYTHONPATH': ':'.join(python_path_list)}
+
+  def get_install_ota_command_list(self):
+    cmd = ('python %s/test/ota/ota_update.py '
+           '--bypass_init_availability_check '
+           '--ota_package=%s/ota.zip '
+           '--mandate_external_storage '
+           '--use_tunnel '
+           '--expected_build=%s '
+           '--expected_build_flavor=%s '
+           '--device_id=%s '
+           '--debug ;' % (
+               self._work_dir,
+               self.get_tmp_dir(),
+               self._property_dict['build_number'],
+               self._property_dict['build_flavor'],
+               self._property_dict['device_id']))
+    return shlex.split(cmd)
+
+  def get_copy_dependencies_command_list(self):
+    slave_name = os.path.basename(self.get_tmp_dir())
+    cmd = ('python %s/test/unit_test/prep_device.py '
+           '--device_id=%s '
+           '--repo_alias=%s '
+           '--branch_name=%s '
+           '--target_name=%s-%s '
+           '--build_number=%s '
+           '--slave_name=%s '
+           '--test_binary_dir=%s/unit_test_binaries '
+           '--debug' % (
+               self._work_dir,
+               self._property_dict['device_id'],
+               self._property_dict['repo_alias'],
+               self._property_dict['branch_name'],
+               self._property_dict['product_name'],
+               self._property_dict['build_flavor'],
+               self._property_dict['build_number'],
+               slave_name,
+               self.get_tmp_dir()))
+    return shlex.split(cmd)
+
+  def get_run_test_command_list(self):
+    slave_name = os.path.basename(self.get_tmp_dir())
+    cmd = ('python3 %s/test/unit_test/run_unit_test_2.py '
+           '--debug '
+           '--use_tunnel '
+           '--repo_alias=%s '
+           '--device_id=%s '
+           '--branch_name=%s '
+           '--report_directory_on_workstation=%s '
+           '--unittest_command_list_file=%s/%s/run_test_list.txt '
+           '--test_binary_dir=%s/unit_test_binaries '
+           '--cloud_logs_path=%s' % (
+               self._work_dir,
+               self._property_dict['repo_alias'],
+               self._property_dict['device_id'],
+               self._property_dict['branch_name'],
+               self._property_dict['local_log_directory'],
+               self._work_dir,
+               slave_name,
+               self.get_tmp_dir(),
+               self._property_dict['cloud_logs_path']))
+    return shlex.split(cmd)
+
+  def get_setup_steps(self):
+    # Bypass all the repo operations
+    return []
+
+  def get_steps(self):
+    slave_name = os.path.basename(self.get_tmp_dir())
+
+    return [
+        fetch_ota_step.FetchOtaStep(
+            branch=self._property_dict['branch_name'],
+            product=self._property_dict['product_name'],
+            board=self._property_dict['board_type'],
+            flavor=self._property_dict['build_flavor'],
+            work_dir=self.get_tmp_dir(),
+            **self._step_kwargs),
+        device_unit_test_step.UnitTestShellStep(
+            name='installing',
+            command=self.get_install_ota_command_list(),
+            directory=self._work_dir,
+            env=self.get_python_path_dict(),
+            halt_on_failure=True,
+            **self._step_kwargs),
+        device_unit_test_step.UnitTestShellStep(
+            name='Create unit test binary directory',
+            command=['mkdir', '%s/unit_test_binaries' % self.get_tmp_dir()],
+            directory=self._work_dir,
+            env=self.get_python_path_dict(),
+            halt_on_failure=True,
+            **self._step_kwargs),
+        device_unit_test_step.UnitTestShellStep(
+            name='Copy test dependencies to device',
+            command=self.get_copy_dependencies_command_list(),
+            directory=self._work_dir,
+            env=self.get_python_path_dict(),
+            halt_on_failure=True,
+            **self._step_kwargs),
+        device_unit_test_step.UnitTestShellStep(
+            name='Run unit test on device',
+            command=self.get_run_test_command_list(),
+            directory=self._work_dir,
+            env=self.get_python_path_dict(),
+            halt_on_failure=True,
+            **self._step_kwargs),
+        device_unit_test_step.UnitTestShellStep(
+            name='Clean up unit test directory',
+            command=['rm', '-rf', '%s/%s' % (self._work_dir, slave_name)],
+            directory=self._work_dir,
+            env=self.get_python_path_dict(),
+            halt_on_failure=True,
+            **self._step_kwargs)
+    ]
+
diff --git a/cq/scripts/slave/recipe/dipper_builder.py b/cq/scripts/slave/recipe/dipper_builder.py
new file mode 100644
index 0000000..e130eb8
--- /dev/null
+++ b/cq/scripts/slave/recipe/dipper_builder.py
@@ -0,0 +1,35 @@
+"""Recipe for running a Dipper build on the given device."""
+import os
+
+from slave import base_recipe
+from slave.step import dipper_step
+
+
+
+def GetValidBuildNames():
+  """Gets all valid build names."""
+  return ['dipper-eng', 'dipper-user']
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  return DipperRecipe(build_name, **kwargs)
+
+
+class DipperRecipe(base_recipe.BaseRecipe):
+  """Recipe to run Dipper build."""
+
+  def __init__(self, build_name, **kwargs):
+    prop = kwargs.get('properties', {})
+    prop['skip_gclient'] = True
+    if 'properties' not in kwargs:
+      kwargs['properties'] = prop
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+    self._build_name = build_name
+
+  def get_steps(self):
+    return [
+        dipper_step.DipperStep(
+            self._build_name,
+            **self._step_kwargs),
+    ]
diff --git a/cq/scripts/slave/recipe/docker_testing_image_runner.py b/cq/scripts/slave/recipe/docker_testing_image_runner.py
new file mode 100644
index 0000000..71eb6e9
--- /dev/null
+++ b/cq/scripts/slave/recipe/docker_testing_image_runner.py
@@ -0,0 +1,63 @@
+"""Recipe for building and depoying the docker image used by the testing team.
+"""
+import os
+
+from slave import base_recipe
+from slave.step import docker_testing_image_step
+from slave.step import docker_push_testing_step
+
+DOCKER_INFORMATION = {
+  'docker_testing_image': {
+    'bucket': 'gcr.io/google.com/home-ci-controller-staging/home-testing',
+    'path_info': {
+        'project': 'test',
+        'dockerfile': 'docker_images'} # Path in project to Dockerfile
+  }
+}
+
+def GetValidBuildNames():
+  return ['docker_testing_image',]
+
+def CreateRecipe(build_name: str, **kwargs):
+  bucket = DOCKER_INFORMATION[build_name]['bucket']
+  path_info = DOCKER_INFORMATION[build_name]['path_info']
+  return DockerTestingImageRecipe(
+      bucket=bucket,
+      path_info=path_info,
+      **kwargs)
+
+class DockerTestingImageRecipe(base_recipe.BaseRecipe):
+  """Recipe to build and push the Docker image that will be used by the home
+  testing team.
+
+  Args:
+    bucket: The URL of the docker bucket where the image will be pushed and
+    stored
+    path_info: Dictionary to have the project and internal path to the
+    Dockerfile. For example:
+      'path_info': {
+        'project': 'project_repo',
+        'dockerfile': 'path/to/dockerfile/folder'
+      }
+
+  Returns:
+    Steps for build and push docker images.
+  """
+
+  def __init__(self, bucket, path_info, **kwargs):
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+    self._path_info = path_info
+    self._docker_bucket = bucket
+
+  def get_steps(self):
+    steps = [
+      docker_testing_image_step.DockerImageBuildStep(
+        self._docker_bucket,
+        self._path_info,
+        **self._step_kwargs),
+      docker_push_testing_step.DockerPushTestingStep(
+        self._docker_bucket,
+        self._path_info,
+        **self._step_kwargs)
+    ]
+    return steps
diff --git a/cq/scripts/slave/recipe/eureka_kythe_builder.py b/cq/scripts/slave/recipe/eureka_kythe_builder.py
new file mode 100644
index 0000000..e87ec3f
--- /dev/null
+++ b/cq/scripts/slave/recipe/eureka_kythe_builder.py
@@ -0,0 +1,142 @@
+"""Recipe to create and publish KZIPs for Kythe.
+
+See go/eureka-cs-indexing for more context.
+"""
+import copy
+import os
+
+from slave import base_recipe
+from slave.step import gcs_upload_step
+from slave.step import gn_step
+from slave.step import kythe_step
+from slave.step import make_clean_step
+from slave.step import ninja_step
+from slave.test import test_generator
+
+
+# WARNING: the build_args_product and build_args_flavor are used to construct
+# the naming of the out folder. As per the vnames.json file, Kythe expects this
+# exact naming in order to properly index and serve generated files.
+BUILD_CONFIGS = {
+    'fuchsia_eureka_kythe_prod': {
+        'build_args_product': 'fuchsia_arm64_user',
+        'build_args_flavor': 'Release',
+        'build_args': ['is_official_build=true'],
+        'build_target': 'cast_agent_exe',
+        'gcs_folder': 'prod'
+    },
+    'fuchsia_eureka_kythe_staging': {
+        'build_args_product': 'fuchsia_arm64_user',
+        'build_args_flavor': 'Release',
+        'build_args': ['is_official_build=true'],
+        'build_target': 'cast_agent_exe',
+        'gcs_folder': 'staging'
+    },
+}
+
+GCS_UPLOAD_BUCKET = 'eureka-kythe'
+
+# Expect that merged KZIPs are at least 1 GB
+MIN_EXPECTED_KZIP_SIZE = 1 * 1024 * 1024 * 1024
+
+
+def GetValidBuildNames():
+  return list(BUILD_CONFIGS.keys())
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  config = BUILD_CONFIGS[build_name]
+  return EurekaKytheRecipe(config, **kwargs)
+
+
+class EurekaKytheRecipe(base_recipe.BaseRecipe):
+
+  def __init__(self, config, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._build_config = config
+
+  def get_clean_steps(self):
+    return [make_clean_step.MakeCleanStep(**self._step_kwargs)]
+
+  # Needed so that we can properly mock expectations in test.
+  def get_num_jobs(self):  # pylint:disable=method-hidden
+    """Count of jobs to use."""
+    setup_steps = self.get_setup_steps()
+    return setup_steps[0].get_num_jobs(multiplier=10)
+
+  def get_steps(self):
+    steps = []
+
+    cwd = os.getcwd()
+    continuous_tests_root = os.path.abspath(
+        self.get_setup_steps()[0].get_project_path('continuous-tests'))
+    chromium_root = self.get_setup_steps()[0].get_project_path('chromium/src')
+    build_args = copy.deepcopy(self._build_config.get('build_args'))
+    build_args_product = self._build_config.get('build_args_product')
+    build_args_flavor = self._build_config.get('build_args_flavor')
+    build_target = self._build_config.get('build_target')
+    gcs_folder = self._build_config.get('gcs_folder')
+    out_dir = os.path.join(chromium_root, 'out_{}'.format(build_args_product),
+                           build_args_flavor)
+
+    # STEP: Generate gn gen for build
+    gn_gen_step = gn_step.GnGenStep(
+        build_args_product=build_args_product,
+        build_args=build_args,
+        out_dir=out_dir,
+        halt_on_failure=True,
+        cwd=cwd,
+        **self._step_kwargs)
+    steps.append(gn_gen_step)
+
+    # STEP: List gn args
+    gn_args_step = gn_step.GnArgsStep(
+        out_dir=out_dir, cwd=cwd, halt_on_failure=True, **self._step_kwargs)
+    steps.append(gn_args_step)
+
+    # STEP: Generate ninja build
+    num_jobs = self.get_num_jobs()
+    ninja_build_step = ninja_step.NinjaStep(
+        out_dir=out_dir,
+        target=build_target,
+        cwd=cwd,
+        jobs=num_jobs,
+        halt_on_failure=True,
+        **self._step_kwargs)
+    steps.append(ninja_build_step)
+
+    # STEP: Generate compilation database
+    gn_gen_step = gn_step.GnGenCompilationDBStep(
+        out_dir=out_dir,
+        cwd=cwd,
+        build_args_product=build_args_product,
+        target_name=build_target,
+        halt_on_failure=True,
+        **self._step_kwargs)
+    compdb_file = gn_gen_step.get_compdb_file_location()
+    steps.append(gn_gen_step)
+
+    # STEP: Generate KZIPs
+    kzip_gen_step = kythe_step.GenerateKzipStep(
+        cwd=cwd,
+        tests_root=continuous_tests_root,
+        chromium_root=chromium_root,
+        out_dir=out_dir,
+        compdb_file=compdb_file,
+        min_expected_kzip_bytes=MIN_EXPECTED_KZIP_SIZE,
+        halt_on_failures=True,
+        **self._step_kwargs)
+    kzip_file = kzip_gen_step.get_kythe_output_file()
+    steps.append(kzip_gen_step)
+
+    # STEP: Upload to GCS
+    upload_step = gcs_upload_step.GcsUploadStep(
+        gcs_bucket=GCS_UPLOAD_BUCKET,
+        gcs_path=gcs_folder,
+        src_path=kzip_file,
+        **self._step_kwargs)
+    steps.append(upload_step)
+
+    return steps
+
diff --git a/cq/scripts/slave/recipe/eurtest_e2e_test_runner.py b/cq/scripts/slave/recipe/eurtest_e2e_test_runner.py
new file mode 100644
index 0000000..90ca487
--- /dev/null
+++ b/cq/scripts/slave/recipe/eurtest_e2e_test_runner.py
@@ -0,0 +1,85 @@
+# Lint as: python3
+"""Recipe for running eurtest e2e tests against a particular build."""
+import os
+
+from slave import base_recipe
+from slave.recipe import clang_builder
+from slave.step import cast_shell_step
+from slave.step import py_env_setup_step
+from slave.step import py_test_step
+
+
+DEFAULT_E2E_TEST_TIMEOUT_SECONDS = 3600
+SHELL_TO_BUILD = 'displayassistant_grte_opal_eng'
+TEST_FILE_FILTER_REGEX = (
+    r'test/eurtest/tests/assistant/presubmit_tests/.*_test_case.py$')
+
+
+def GetValidBuildNames():
+  return ['py_e2e_test_eurtest_assistant']
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  if build_name != 'py_e2e_test_eurtest_assistant':
+    raise base_recipe.BuildNameNotSupportedError(build_name)
+  return EurtestE2ERecipe(**kwargs)
+
+
+class EurtestE2ERecipe(base_recipe.BaseRecipe):
+  """Recipe to run e2e tests on a repository."""
+
+  def __init__(self,
+               **kwargs):
+    super().__init__(
+        enable_build_accelerator=True,
+        **kwargs)
+
+
+  def get_steps(self):
+    build_shell_config = clang_builder.BUILD_CONFIGS['displayassistant_grte_opal_eng']
+
+    build_branding = build_shell_config.get('build_branding', None)
+    build_arch = build_shell_config.get('build_arch', None)
+    build_product = build_shell_config.get('build_product', None)
+    build_flavor = build_shell_config.get('build_flavor', None)
+    enable_assistant = build_shell_config.get('enable_assistant', False)
+    build_args_official = build_shell_config.get('build_args_official', None)
+    grte_compatible = build_shell_config.get('grte_compatible', False)
+    build_args_product = build_shell_config.get('build_args_product', None)
+    additional_import_paths = [
+        self._eureka_root / path for path
+        in (
+            'builder/masters',
+            'builder/masters/src',
+            'chromium/src/third_party/catapult/telemetry',
+            'chromium/src/chromecast/internal',
+            'chromium/src/chromecast/tools',
+            'continuous-tests',
+            'continuous-tests/cq/scripts',
+            'test',
+            'testing/external/autotest',
+            'testing/external/chromite',
+            'testing/sprockets',
+         )]
+    return [
+        cast_shell_step.CastShellStep(
+            build_branding, build_arch, build_product,
+            build_flavor, enable_assistant,
+            build_args_official=build_args_official,
+            code_coverage=False, grte_compatible=grte_compatible,
+            build_args_product=build_args_product, halt_on_failure=True,
+            sanitizer=None, archive_to_tmp=True, **self._step_kwargs),
+        py_env_setup_step.PyEnvSetupStep(
+            additional_import_paths=additional_import_paths,
+            local_packages_dir='team-catatester/python_packages',
+            py3=True,
+            requirements_path='lab_system/requirements3.txt',
+            requirements_project='test',
+            **self._step_kwargs),
+        py_test_step.PyTestStep(
+            project='test',
+            test_file_filter_regex=TEST_FILE_FILTER_REGEX,
+            timeout=DEFAULT_E2E_TEST_TIMEOUT_SECONDS,
+            **self._step_kwargs)
+    ]
diff --git a/cq/scripts/slave/recipe/fuchsia_builder.py b/cq/scripts/slave/recipe/fuchsia_builder.py
new file mode 100644
index 0000000..8e70f32
--- /dev/null
+++ b/cq/scripts/slave/recipe/fuchsia_builder.py
@@ -0,0 +1,294 @@
+"""Recipe to build and test fuchsia-cast."""
+import copy
+import os
+import re
+import tempfile
+
+from helpers import branch_utils
+from slave import base_recipe
+from slave.step import cipd_upload_step
+from slave.step import fuchsia_cipd_upload_step
+from slave.step import fuchsia_emulator_step
+from slave.step import fuchsia_unittest_step
+from slave.step import gn_step
+from slave.step import landmine_step
+from slave.step import make_clean_step
+from slave.step import ninja_step
+from slave.step import shell_step
+from slave.test import test_generator
+
+
+BUILD_CONFIGS = {
+    'arm64_fuchsia': {
+        'architecture': 'arm64',
+        'build_args_product': 'fuchsia_arm64_user',
+        'build_args_flavor': 'Release',
+        'build_product': 'display',
+        'build_target': 'chromecast/internal/fuchsia:all',
+        'test_target': 'chromecast/internal/fuchsia:chromecast_fuchsia_tests',
+        'run_gtests': False,
+        'run_catatester': True,
+        'build_args': []
+    },
+    'arm64_fuchsia_eng': {
+        'architecture': 'arm64',
+        'build_args_product': 'fuchsia_arm64_eng',
+        'build_args_flavor': 'Eng',
+        'build_product': 'display',
+        'build_target': 'chromecast/internal/fuchsia:all',
+        'test_target': 'chromecast/internal/fuchsia:chromecast_fuchsia_tests',
+        'run_gtests': False,
+        'build_args': []
+    },
+    'arm64_fuchsia_eng_optimized': {
+        'architecture': 'arm64',
+        'build_args_product': 'fuchsia_arm64_eng_optimized',
+        'build_args_flavor': 'EngOptimized',
+        'build_product': 'display',
+        'build_target': 'chromecast/internal/fuchsia:all',
+        'test_target': 'chromecast/internal/fuchsia:chromecast_fuchsia_tests',
+        'run_gtests': False,
+        'build_args': []
+    },
+    'x64_fuchsia': {
+        'architecture': 'amd64',
+        'build_args_product': 'fuchsia_x64_user',
+        'build_args_flavor': 'Release',
+        'build_product': 'display',
+        'build_target': 'chromecast/internal/fuchsia:all',
+        'test_target': 'chromecast/internal/fuchsia:chromecast_fuchsia_tests',
+        'run_gtests': True,
+        'build_args': ['dcheck_always_on=true']
+    },
+    'x64_fuchsia_eng': {
+        'architecture': 'amd64',
+        'build_args_product': 'fuchsia_x64_eng',
+        'build_args_flavor': 'Eng',
+        'build_product': 'display',
+        'build_target': 'chromecast/internal/fuchsia:all',
+        'test_target': 'chromecast/internal/fuchsia:chromecast_fuchsia_tests',
+        'run_gtests': True,
+        'build_args': []
+    },
+    'x64_fuchsia_eng_optimized': {
+        'architecture': 'amd64',
+        'build_args_product': 'fuchsia_x64_eng_optimized',
+        'build_args_flavor': 'EngOptimized',
+        'build_product': 'display',
+        'build_target': 'chromecast/internal/fuchsia:all',
+        'test_target': 'chromecast/internal/fuchsia:chromecast_fuchsia_tests',
+        'run_gtests': True,
+        'build_args': []
+    },
+}
+
+SHORTLIVED_BRANCH_PATTERN = (
+    r'(?P<major>\d+)\.(?P<minor>\d+)_shortlived_(?P<build>\d+)')
+
+CIPD_UPLOADS_FILE_NAME = 'cipd_uploads'
+
+
+def GetValidBuildNames():
+  return list(BUILD_CONFIGS.keys())
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  config = BUILD_CONFIGS[build_name]
+  return FuchsiaRecipe(config, **kwargs)
+
+
+class FuchsiaRecipe(base_recipe.BaseRecipe):
+  """Recipe to lint a repository."""
+
+  def __init__(self, config, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._build_config = config
+    self._fuchsia_workdir = None
+    self._properties = kwargs.get('properties', {})
+
+  def get_fuchsia_bucket(self, chromium_root):
+    """Get the current Fuchsia bucket to get artifacts from.
+
+    Usually, this will pull artifacts from either the public or private Fuchsia
+    SDK buckets. For go/feureka-dd builds, it will pull from a temporary bucket.
+
+    Args:
+      chromium_root: Path to chromium source directory.
+
+    Returns:
+      GCP bucket. For example: 'fuchsia-sdk' or 'fuchsia'.
+    """
+    bucket_path = os.path.join(chromium_root, 'build/fuchsia/sdk-bucket.txt')
+    bucket = 'fuchsia'
+    # Only go/feureka-dd writes to the bucket file
+    with open(bucket_path) as f:
+      # Trim any whitespace on each line
+      bucket = f.readline().rstrip()
+    return bucket or 'fuchsia'
+
+  def get_num_jobs(self):  # pylint:disable=method-hidden
+    """Count of jobs to use."""
+    setup_steps = self.get_setup_steps()
+    return setup_steps[0].get_num_jobs(multiplier=10)
+
+  def create_temp_directory(self):
+    return tempfile.TemporaryDirectory().name
+
+  def get_steps(self):
+    # Use one of the setup steps to get chromium root
+    cwd = os.getcwd()
+    setup_steps = self.get_setup_steps()
+    chromium_root = setup_steps[0].get_project_path('chromium/src')
+    gcs_dir = setup_steps[0].get_gcs_dir()
+    jobs = self.get_num_jobs()
+    architecture = self._build_config.get('architecture')
+    build_args_product = self._build_config.get('build_args_product')
+    build_args_flavor = self._build_config.get('build_args_flavor')
+    build_args = copy.deepcopy(self._build_config.get('build_args'))
+    if self.build_system == 'catabuilder':
+      build_args.append('is_official_build=true')
+    build_target = self._build_config.get('build_target')
+    test_target = self._build_config.get('test_target')
+    run_gtests = self._build_config.get('run_gtests')
+    run_catatester = self._build_config.get('run_catatester', False)
+    post_submit_upload = self.build_system == 'catabuilder'
+    self._fuchsia_sdk_root = os.path.join(chromium_root,
+                                          'third_party/fuchsia-sdk/')
+    self._fuchsia_workdir = self.create_temp_directory()
+
+    # Extend buildargs with incremental build id
+    incremental_build_prop = ('cast_build_incremental="{}"'.format(
+        self.build_number))
+    build_args.append(incremental_build_prop)
+    out_dir = os.path.join(chromium_root, 'out_{}'.format(build_args_product),
+                           build_args_flavor)
+    steps = []
+    if self.build_system == 'catabuilder':
+      steps.append(make_clean_step.MakeCleanStep(**self._step_kwargs))
+    else:
+      steps += [
+          landmine_step.LandmineStep(**self._step_kwargs),
+      ]
+    # Generate gn gen step
+    gn_gen_step = gn_step.GnGenStep(
+        build_args_product=build_args_product,
+        build_args=build_args,
+        out_dir=out_dir,
+        halt_on_failure=True,
+        cwd=cwd,
+        **self._step_kwargs)
+    steps.append(gn_gen_step)
+    # Generate gn args step
+    gn_args_step = gn_step.GnArgsStep(
+        out_dir=out_dir, cwd=cwd, halt_on_failure=True, **self._step_kwargs)
+    steps.append(gn_args_step)
+    # Generate ninja build step
+    ninja_build_step = ninja_step.NinjaStep(
+        out_dir=out_dir,
+        target=build_target,
+        cwd=cwd,
+        jobs=jobs,
+        halt_on_failure=True,
+        **self._step_kwargs)
+    steps.append(ninja_build_step)
+    # Generate ninja build tests step
+    max_timeout_secs = 4 * 60 * 60
+    ninja_build_tests_step = ninja_step.NinjaStep(
+        out_dir=out_dir,
+        target=test_target,
+        halt_on_failure=True,
+        timeout_secs=max_timeout_secs,
+        cwd=cwd,
+        jobs=jobs,
+        **self._step_kwargs)
+    steps.append(ninja_build_tests_step)
+    if run_gtests:
+      bucket = self.get_fuchsia_bucket(chromium_root)
+
+      # Start FEMU
+      femu_start_step = fuchsia_emulator_step.FuchsiaEmulatorStartStep(
+          bucket=bucket,
+          fuchsia_sdk_root=self._fuchsia_sdk_root,
+          fuchsia_workdir=self._fuchsia_workdir,
+          **self._step_kwargs)
+      steps.append(femu_start_step)
+
+      step = fuchsia_unittest_step.FuchsiaUnitTestStep(
+          out_dir=out_dir,
+          fuchsia_workdir=self._fuchsia_workdir,
+          recipe_steps=steps,
+          cwd=cwd,
+          **self._step_kwargs)
+      steps.append(step)
+
+    # Upload the artifacts to CIPD
+    refs = {self._manifest_branch}
+
+    tags = {
+        'version': self.build_number,
+    }
+
+    far_pkg_vars = {
+      'targetarch': architecture
+    }
+
+    cipd_uploads_file = os.path.join(out_dir, 'gen', CIPD_UPLOADS_FILE_NAME)
+    cipd_yaml_dir = chromium_root
+    steps.append(fuchsia_cipd_upload_step.FuchsiaCIPDUploadStep(
+        steps, cipd_uploads_file, cipd_yaml_dir,
+        refs=refs,
+        tags=tags,
+        pkg_vars=far_pkg_vars,
+        **self._step_kwargs))
+
+    # Package gunit test dependencies and upload to gcs
+    # only if recipe is running from Catabuilder.
+    if post_submit_upload and run_catatester:
+      max_timeout_secs = 30 * 60
+      ninja_test_deps_archive_step = ninja_step.NinjaStep(
+          out_dir=out_dir,
+          target='chromecast_test_deps_archive',
+          halt_on_failure=True,
+          timeout_secs=max_timeout_secs,
+          cwd=cwd,
+          jobs=jobs,
+          **self._step_kwargs)
+      steps.append(ninja_test_deps_archive_step)
+
+      src_path = os.path.join(out_dir, 'test_deps.tar.gz')
+      dst_path = os.path.join(gcs_dir, 'artifacts', 'tests')
+      copy_deps_steps = [
+          shell_step.ShellStep(
+              name='Create test deps destination folder',
+              command=[
+                  'mkdir',
+                  '-p',
+                  dst_path,
+              ],
+              directory=os.getcwd(),
+              **self._step_kwargs),
+          shell_step.ShellStep(
+              name='Move test_deps.tar.gz to upload folder',
+              command=['mv', src_path, dst_path],
+              directory=os.getcwd(),
+              **self._step_kwargs)
+      ]
+      steps.extend(copy_deps_steps)
+    return steps
+
+  def get_teardown_steps(self):
+    """Returns a list of all steps that should be executed as part of teardown.
+
+    Teardown steps are always called, even if previous steps fail.
+
+    Returns:
+      List of slave.base_step.BaseStep commands to be executed.
+    """
+    return [
+      fuchsia_emulator_step.FuchsiaEmulatorTeardownStep(
+            fuchsia_sdk_root=self._fuchsia_sdk_root,
+            fuchsia_workdir=self._fuchsia_workdir,
+            **self._step_kwargs)
+    ] + super().get_teardown_steps()
diff --git a/cq/scripts/slave/recipe/gn_check.py b/cq/scripts/slave/recipe/gn_check.py
new file mode 100644
index 0000000..40eef22
--- /dev/null
+++ b/cq/scripts/slave/recipe/gn_check.py
@@ -0,0 +1,22 @@
+"""Recipe for running a gn check on the given build."""
+from slave import base_recipe
+from slave.step import gn_check_step
+
+
+def GetValidBuildNames():
+  return ['gn_check']
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  del build_name  # Unused.
+  return GnCheckRecipe(**kwargs)
+
+
+class GnCheckRecipe(base_recipe.BaseRecipe):
+  """Recipe to gn check a change."""
+
+  def get_steps(self):
+    return [
+        gn_check_step.GnCheckStep(**self._step_kwargs)
+    ]
diff --git a/cq/scripts/slave/recipe/gotham_builder.py b/cq/scripts/slave/recipe/gotham_builder.py
new file mode 100644
index 0000000..b3b3449
--- /dev/null
+++ b/cq/scripts/slave/recipe/gotham_builder.py
@@ -0,0 +1,248 @@
+"""Recipe for building gotham."""
+import os
+
+from slave import base_recipe
+from slave.step import gotham_build_step
+from slave.step import make_clean_step
+from slave.step import py_env_setup_step
+from slave.test import test_generator
+
+
+# When adding new boards they also need to be added to:
+#     - build_specs.json
+#     - build_details.protoascii (Staging & Prod)
+#     - build_rules.protoascii (Staging & Prod)
+#     - run 'cq/scripts/tools/recipes_test.py --train' to
+#       generate recipe_expectation/*.json
+
+BUILD_CONFIGS = {
+    # Legacy naming convention.
+    'gotham_bootloader': {
+        'build_name': 'bootloader',
+        'board': 'gotham_bootloader',
+    },
+    'gotham_bootloader_16mb': {
+        'build_name': 'bootloader',
+        'board': 'gotham_bootloader_16mb',
+    },
+    'gotham_eng': {
+        'build_name': 'eng',
+        'board': '',
+    },
+    'gotham_eng-arkham2': {
+        'build_name': 'eng',
+        'board': 'arkham2',
+    },
+    'gotham_eng-gotham_evt': {
+        'build_name': 'eng',
+        'board': 'gotham_evt',
+    },
+    'gotham_eng-gotham_p2': {
+        'build_name': 'eng',
+        'board': 'gotham_p2',
+    },
+    'gotham_factory': {
+        'build_name': 'factory',
+        'board': '',
+    },
+    'gotham_factory-arkham2': {
+        'build_name': 'factory',
+        'board': 'arkham2',
+    },
+    'gotham_factory-gotham_evt': {
+        'build_name': 'factory',
+        'board': 'gotham_evt',
+    },
+    'gotham_factory-gotham_p2': {
+        'build_name': 'factory',
+        'board': 'gotham_p2',
+    },
+    'gotham_test': {
+        'build_name': 'test',
+        'board': '',
+    },
+    'gotham_test-arkham2': {
+        'build_name': 'test',
+        'board': 'arkham2',
+    },
+    'gotham_test-gotham_evt': {
+        'build_name': 'test',
+        'board': 'gotham_evt',
+    },
+    'gotham_test-gotham_p2': {
+        'build_name': 'test',
+        'board': 'gotham_p2',
+    },
+    'gotham_test-host': {
+        'build_name': 'test',
+        'board': 'host',
+    },
+
+    # Current naming convention.
+    'gotham_mcu-bootloader-eng': {
+        'build_name': 'bootloader-eng',
+        'board': '',
+    },
+    'gotham_mcu-bootloader-user': {
+        'build_name': 'bootloader-user',
+        'board': '',
+    },
+    'gotham_mcu-eng': {
+        'build_name': 'eng',
+        'board': '',
+    },
+    'gotham_mcu-eng-gotham_dvt': {
+        'build_name': 'eng',
+        'board': 'gotham_dvt',
+    },
+    'gotham_mcu-eng-metrics-gotham_dvt': {
+        'build_name': 'metrics',
+        'board': '',
+    },
+    'gotham_mcu-eng-gotham_evt': {
+        'build_name': 'eng',
+        'board': 'gotham_evt',
+    },
+    'gotham_mcu-user': {
+        'build_name': 'user',
+        'board': '',
+    },
+    'gotham_mcu-user-gotham_dvt': {
+        'build_name': 'user',
+        'board': 'gotham_dvt',
+    },
+    'gotham_mcu-user-gotham_evt': {
+        'build_name': 'user',
+        'board': 'gotham_evt',
+    },
+    'gotham_mcu-fsi-eng': {
+        'build_name': 'fsi-eng',
+        'board': '',
+    },
+    'gotham_mcu-fsi-eng-gotham_dvt': {
+        'build_name': 'fsi-eng',
+        'board': 'gotham_dvt',
+    },
+    'gotham_mcu-fsi-eng-gothm_evt': {
+        'build_name': 'fsi-eng',
+        'board': 'gothm_evt',
+    },
+    'gotham_mcu-fsi-user': {
+        'build_name': 'fsi-user',
+        'board': '',
+    },
+    'gotham_mcu-fsi-user-gotham_dvt': {
+        'build_name': 'fsi-user',
+        'board': 'gotham_dvt',
+    },
+    'gotham_mcu-fsi-user-gotham_evt': {
+        'build_name': 'fsi-user',
+        'board': 'gotham_evt',
+    },
+    'gotham_mcu-factory': {
+        'build_name': 'factory',
+        'board': '',
+    },
+    'gotham_mcu-factory-arkham2': {
+        'build_name': 'factory',
+        'board': 'arkham2',
+    },
+    'gotham_mcu-factory-gotham_dvt': {
+        'build_name': 'factory',
+        'board': 'gotham_dvt',
+    },
+    'gotham_mcu-factory-gotham_evt': {
+        'build_name': 'factory',
+        'board': 'gotham_evt',
+    },
+    'gotham_mcu-rma-unlock': {
+        'build_name': 'rma-unlock',
+        'board': '',
+    },
+    'gotham_mcu-test': {
+        'build_name': 'test',
+        'board': '',
+    },
+    'gotham_mcu-test-gotham_dvt': {
+        'build_name': 'test',
+        'board': 'gotham_dvt',
+    },
+    'gotham_mcu-test-gotham_evt': {
+        'build_name': 'test',
+        'board': 'gotham_evt',
+    },
+    'gotham_mcu-test-host': {
+        'build_name': 'test',
+        'board': 'host',
+    },
+    'gotham_linux_x64': {
+        'build_name': 'linux',
+        'board': '',
+    },
+    'gotham_mcu-dynamic-analysis': {
+        'build_name': 'dynamic-analysis',
+        'board': '',
+    }
+}
+
+
+def GetValidBuildNames():
+  return list(BUILD_CONFIGS.keys())
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  return GothamRecipe(build_name, **kwargs)
+
+
+class GothamRecipe(base_recipe.BaseRecipe):
+  """Recipe to build gotham using the build_gotham."""
+
+  def __init__(self, build_name, **kwargs):
+    prop = kwargs.get('properties', {})
+    prop['skip_gclient'] = True
+    if 'properties' not in kwargs:
+      kwargs['properties'] = prop
+    self._executor = kwargs['executor']
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+
+    self._build_name = BUILD_CONFIGS[build_name]['build_name']
+    self._board = BUILD_CONFIGS[build_name]['board']
+
+  def _get_clean_dir(self):
+    if 'bootloader' in self._build_name:
+      return 'gotham/source/bootloader'
+    if 'factory' in self._build_name:
+      return 'gotham/source/fct'
+    if 'rma-unlock' in self._build_name:
+      return 'gotham/source/rma_unlock'
+    return 'gotham/source/app'
+
+  def get_steps(self):
+    clean_dir = self._get_clean_dir()
+    requirements_path = os.path.join('tools/build_tools/', 'requires.txt')
+    gotham_python_packages = 'team-gotham/python_packages'
+
+    steps = []
+
+    # TODO(b/147378899, b/147672428) include landmine support
+    if self.build_system == 'catabuilder':
+      steps.append(make_clean_step.MakeCleanStep(
+          clean_dir=clean_dir, **self._step_kwargs))
+
+    steps.extend([
+        py_env_setup_step.PyEnvSetupStep(
+            local_packages_dir=gotham_python_packages,
+            py3=True,
+            requirements_path=requirements_path,
+            requirements_project='gotham-cr/gotham',
+            **self._step_kwargs),
+        gotham_build_step.GothamBuildStep(
+            build_name=self._build_name,
+            board=self._board,
+            name='build',
+            cwd='gotham/tools/buildbot/',
+            **self._step_kwargs)
+    ])
+
+    return steps
+
diff --git a/cq/scripts/slave/recipe/include_what_you_use.py b/cq/scripts/slave/recipe/include_what_you_use.py
new file mode 100644
index 0000000..5205734
--- /dev/null
+++ b/cq/scripts/slave/recipe/include_what_you_use.py
@@ -0,0 +1,98 @@
+"""Recipe for giving include-what-you-use (iwyu) suggestions on the given build."""
+
+# TODO(mingcl): This recipe is WIP
+
+import logging
+from slave import base_recipe
+from slave import base_step
+from slave.step import include_what_you_use_step
+from slave.step import compdb_gen_step
+from slave.step import cast_shell_step
+from slave.step import unittest_step
+from slave.recipe import clang_builder
+
+# Note that the keys are Gerrit project names.
+# TODO(mingcl): Allow configuring build-specific filters to avoid reporting
+# duplicate clang-tidy suggestions from different builds.
+PROJECT_CONFIGS = {
+    'chromecast/internal': {
+        'deny_list_filter': [
+            r'vendor/media/google3/(.*)', 'android/prebuilt',
+            r'(.*)/third_party/(.*)'
+        ]
+    },
+}
+
+BUILD_CONFIGS = {
+    'audioassistant_x64_eng_iwyu': {
+        'build_args_product': 'audioassistant_x64_eng',
+        'build_args_official': False,
+        # TODO(b/145840052): Remove once the build is fast enough.
+        'timeout_secs_build_gtests': 5 * 60 * 60,
+        # TODO(b/148885147) Remove params below
+        'build_branding': 'chrome',
+        'build_product': 'audio',
+        'build_flavor': 'Debug',
+        'build_arch': 'clang',
+        'enable_assistant': True,
+        'run_gtests': False,
+    },
+}
+
+# The name of build directory used by actual build recipe.
+# Since the build directory name could change when the build recipe/script get
+# changed, the best we can do is to update this dictionary when needed.
+# This should be fine since clang tidy check is non-blocking.
+BUILD_DIRECTORIES = {
+    'audioassistant_x64_eng_iwyu': 'out_clang_audio_gn/Debug/',
+}
+
+def GetValidBuildNames():
+  return [
+      'audioassistant_x64_eng_iwyu',
+  ]
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  # Extracts default_properties from build config and adds it to the overall
+  # properties in kwargs.
+  config = BUILD_CONFIGS[build_name]
+  return ClangTidyRecipe(
+      build_name,
+      build_config=config,
+      build_dir=BUILD_DIRECTORIES[build_name],
+      **kwargs)
+
+class ClangTidyRecipe(base_recipe.BaseRecipe):
+  """Recipe to clang-tidy a change."""
+
+  def __init__(self, build_name, build_config, build_dir, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._properties = kwargs.get('properties', {})
+    self._build_name = build_name
+    self._build_config = build_config
+    self._build_dir = build_dir
+    self._builder_kwargs = kwargs
+
+  def get_steps(self):
+    allow_list_filter = PROJECT_CONFIGS.get(self._properties['patch_project'],
+                                           {}).get('allow_list_filter', None)
+    deny_list_filter = PROJECT_CONFIGS.get(self._properties['patch_project'],
+                                           {}).get('deny_list_filter', None)
+
+    # Build cast shell and unittests to generate necessary header files
+    build_steps = clang_builder.ClangRecipe(
+        self._build_config, **self._builder_kwargs).get_steps()
+
+    return build_steps + [
+        # Generate compilation database from the build directory.
+        compdb_gen_step.CompdbGenStep(
+            build_dir=self._build_dir, **self._step_kwargs),
+        # Then iwyu can use the compilation database to analyze the code
+        include_what_you_use_step.IncludeWhatYouUseStep(
+            allow_list_filter=allow_list_filter,
+            deny_list_filter=deny_list_filter,
+            **self._step_kwargs),
+    ]
diff --git a/cq/scripts/slave/recipe/junit_builder.py b/cq/scripts/slave/recipe/junit_builder.py
new file mode 100644
index 0000000..1380e1c
--- /dev/null
+++ b/cq/scripts/slave/recipe/junit_builder.py
@@ -0,0 +1,107 @@
+"""Recipe for building and running JUnit tests."""
+from slave import base_recipe
+from slave.step import unittest_step, jacoco_coverage_processing_step
+
+BUILD_BRANDING = 'chrome'
+BUILD_ARCH = 'x86'
+BUILD_FLAVOR = 'Release'
+BUILD_PRODUCT = 'android'
+
+# This is needed to override the "DEFAULT_MANIFEST_STRING" in test_generator.py
+# by adding the chromium/tools/depot_tools project.
+SAMPLE_MANIFEST_STRING = """
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest>
+  <remote fetch="https://chromium.googlesource.com/" name="chromium" review="https://chromium-review.googlesource.com/"/>
+  <remote fetch="https://eureka-internal.googlesource.com/" name="eureka" review="sso://eureka-internal"/>
+  <remote fetch="https://github.com/" name="github"/>
+
+  <default revision="master"/>
+
+  <project name="chromium/src" remote="eureka" revision="unfork_m43"/>
+  <project name="chromium/tools/depot_tools" remote="chromium"/>
+  <project name="chromiumos/chromite" path="chromite/chromite" remote="chromium"/>
+  <project name="chromiumos/third_party/autotest" path="autotest" remote="chromium"/>
+  <project name="continuous-tests" remote="eureka"/>
+  <project name="fake/unittest" remote="eureka"/>
+  <project name="fragglerock" remote="eureka"/>
+  <project name="prebuilt/toolchain" path="toolchain" remote="eureka"/>
+  <project name="google/sprockets" path="test/sprockets" remote="github"/>
+  <project name="test" remote="eureka"/>
+  <project name="chromecast/internal" path="chromium/src/chromecast/internal" remote="eureka"/>
+  <project name="chromecast/internal/voice_ui" path="chromium/src/chromecast/internal/chirp" remote="eureka"/>
+  <project name="vendor/amlogic" remote="eureka"/>
+  <project name="vendor/marvell" remote="eureka"/>
+</manifest>
+"""
+
+BUILD_CONFIGS = {
+    'junit_androidtv': {
+        'build_args_product': 'atv_x86_user',
+        'build_args_official': False,
+        'code_coverage': True,
+    },
+    'junit_soba': {
+        'build_args_product': 'soba_arm_internal',
+        'build_args_official': False,
+        'code_coverage': True,
+    },
+    'junit_udon': {
+        'build_args_product': 'udon_x64_user',
+        'build_args_official': False,
+        'code_coverage': True,
+    },
+}
+
+
+def GetValidBuildNames():
+  return BUILD_CONFIGS.keys()
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  config = BUILD_CONFIGS[build_name]
+  return JunitRecipe(config, **kwargs)
+
+class JunitRecipe(base_recipe.BaseRecipe):
+  """Recipe to build and run JUnit tests."""
+
+  def __init__(self, config, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._config = config
+
+  def get_steps(self):
+    build_args_product = self._config['build_args_product']
+    build_args_official = self._config['build_args_official']
+    code_coverage = self._config['code_coverage']
+
+    steps = []
+    steps += [
+        unittest_step.BuildAffectedUnitTestsStep(
+            BUILD_BRANDING, BUILD_ARCH, BUILD_PRODUCT,
+            BUILD_FLAVOR, enable_assistant=False, sanitizer=None,
+            build_args_product=build_args_product,
+            build_args_official=build_args_official,
+            halt_on_failure=True, junit=True,
+            code_coverage=code_coverage, **self._step_kwargs),
+        unittest_step.RunAffectedUnitTestsStep(
+            BUILD_BRANDING, BUILD_ARCH, BUILD_PRODUCT, BUILD_FLAVOR,
+            report_url='', recipe_steps=steps, sanitizer=None,
+            build_args_product=build_args_product,
+            build_args_official=build_args_official,
+            enable_network_service=False, junit=True, **self._step_kwargs),
+    ]
+
+    # If code coverage is enabled we must process coverage
+    # files and generate a report.
+    if code_coverage:
+      steps.append(
+        jacoco_coverage_processing_step.JacocoCoverageProcessingStep(
+          BUILD_BRANDING, BUILD_ARCH, BUILD_PRODUCT, BUILD_FLAVOR,
+          'coverage',  build_args_product=build_args_product,
+          build_args_official=build_args_official, **self._step_kwargs
+        )
+      )
+
+    return steps
diff --git a/cq/scripts/slave/recipe/libassistant_sdk_validator.py b/cq/scripts/slave/recipe/libassistant_sdk_validator.py
new file mode 100644
index 0000000..a5f5c9d
--- /dev/null
+++ b/cq/scripts/slave/recipe/libassistant_sdk_validator.py
@@ -0,0 +1,50 @@
+"""Recipe to validate libassistant sdk code on the given patch."""
+from slave import base_recipe
+from slave.step import libassistant_sdk_step as sdk_step
+
+# pylint: disable=line-too-long
+SAMPLE_MANIFEST_STRING = """
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest>
+  <remote fetch="https://libassistant-internal.googlesource.com/" name="libassistant" review="https://libassistant-internal-review.googlesource.com/"/>
+
+  <default revision="master"/>
+
+  <project name="standalone/src" path="src" remote="libassistant"/>
+  <project name="standalone/deps" path="src/deps" remote="libassistant"/>
+  <project name="libassistant/contrib" path="src/libassistant/contrib" remote="libassistant"/>
+  <project name="libassistant/shared" path="src/libassistant/shared" remote="libassistant"/>
+  <project name="libassistant/internal" path="src/libassistant/internal" remote="libassistant"/>
+  <project name="libassistant/internal/third_party" path="src/libassistant/internal/third_party" remote="libassistant"/>
+</manifest>
+"""
+# pylint: enable=line-too-long
+
+DEFAULT_PROPERTIES = {'gclient_root': 'src'}
+
+
+def GetValidBuildNames():
+  return ['validate_libassistant_sdk']
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  del build_name
+  return LibassistantSdkRecipe(**kwargs)
+
+
+class LibassistantSdkRecipe(base_recipe.BaseRecipe):
+  """Recipe to validate libassistant sdk."""
+
+  def __init__(self, **kwargs):
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+    # we don't want to run this recipe in CB
+    assert self.build_system == 'cq'
+
+    self._step_kwargs['properties'] = dict(
+        list(DEFAULT_PROPERTIES.items()) +
+        list(self._step_kwargs.get('properties', {}).items())
+    )
+
+  def get_steps(self):
+    return [sdk_step.SdkValidationStep(**self._step_kwargs)]
+
diff --git a/cq/scripts/slave/recipe/luci_recipe_runner.py b/cq/scripts/slave/recipe/luci_recipe_runner.py
new file mode 100644
index 0000000..5dc52d6
--- /dev/null
+++ b/cq/scripts/slave/recipe/luci_recipe_runner.py
@@ -0,0 +1,130 @@
+"""Recipe for running LUCI recipe tests."""
+import logging
+import os
+
+from slave import base_recipe
+from slave import base_step
+from slave.step import cipd_upload_step
+from slave.step import luci_recipe_step
+from slave.step import shell_step
+
+from slave.test import test_generator
+
+PROJECT_CONFIGS = {
+    'test': {
+        # Path to the desired recipes.py script.
+        'script': 'swarming/recipes/recipes.py',
+        # Path to the recipes.cfg file for the recipe package.
+        'package': 'infra/config/recipes.cfg',
+        # luci_recipe_bundle only. Desired out dir for generated bundle.
+        'bundle': 'swarming/recipes/bundle',
+        # luci_recipe_bundle only. Path to the cipd def for the recipe package.
+        'cipd_cfg': 'swarming/recipes/cipd.yaml',
+        'cipd_refs': ['staging'],
+        'test_retries': 0,
+    },
+    # Gazoo recipes.
+    # nest-internal
+    'gazoo/infra': {
+        'script': 'recipes/recipes.py',
+        'package': 'infra/config/recipes.cfg',
+        'bundle': 'recipes/bundle',
+        'cipd_cfg': 'recipes/cipd.yaml',
+        'cipd_refs': ['gazoo-staging'],
+        # Adding retries for flaky coverage report (b/145263501).
+        'test_retries': 4,
+    },
+}
+
+BUILD_CONFIGS = {
+    'luci_recipe_test': {
+        'project': None,  # Project provided by CQ via `patch_project`.
+        'upload_to_cipd': False,
+    },
+    'gazoo_recipes_upload': {
+        'project': 'gazoo/infra',
+        'upload_to_cipd': True,
+    },
+}
+
+
+def GetValidBuildNames():
+  return list(BUILD_CONFIGS.keys())
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  return LUCIRecipeRecipe(build_name, **kwargs)
+
+
+class DoNothingStep(base_step.BaseStep):
+  """Does nothing."""
+  def __init__(self, msg, **kwargs):
+    """Creates a JsonCheckStep instance.
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='nothing to do here.', **kwargs)
+    self._msg = msg
+
+  def run(self):
+    """Print a message and return."""
+    logging.info(self._msg)
+    return True
+
+
+class LUCIRecipeRecipe(base_recipe.BaseRecipe):
+  """Recipe to run LUCI recipe functions."""
+
+  def __init__(self, build_name, **kwargs):
+    super(LUCIRecipeRecipe, self).__init__(**kwargs)
+    self._build_name = build_name
+
+  def get_steps(self):
+    build_config = BUILD_CONFIGS[self._build_name]
+    if build_config.get('project'):
+      # Running on CB. Set patch_project via BUILD_CONFIGS.
+      self._properties['patch_project'] = build_config['project']
+    if not PROJECT_CONFIGS.get(self._properties['patch_project']):
+      return [DoNothingStep('Project {} not in PROJECT_CONFIGS.',
+                            **self._step_kwargs)]
+    project_config = PROJECT_CONFIGS[self._properties['patch_project']]
+    script_path = project_config['script']
+    package_path = project_config['package']
+    test_retries = project_config['test_retries']
+    steps = [
+        luci_recipe_step.LUCIRecipeTestStep(
+            script_path,
+            package_path,
+            test_retries,
+            name='Test recipes',
+            **self._step_kwargs),
+    ]
+
+    if build_config.get('upload_to_cipd'):
+      bundle_path = project_config['bundle']
+      cipd_path = project_config['cipd_cfg']
+      cipd_refs = project_config['cipd_refs']
+      cleanup_cmd = ['rm', '-rf', bundle_path]
+      steps.extend([
+          shell_step.ShellStep(
+              name='Ensure bundle dir does not exist',
+              command=cleanup_cmd,
+              **self._step_kwargs),
+          luci_recipe_step.LUCIRecipeBundleStep(
+              script_path,
+              package_path,
+              bundle_path,
+              name='Bundle recipes',
+              **self._step_kwargs),
+          cipd_upload_step.CIPDUploadStep(
+              os.path.join(self._properties['patch_project'], cipd_path),
+              refs=cipd_refs,
+              **self._step_kwargs),
+          shell_step.ShellStep(
+              name='Cleanup bundle dir',
+              command=cleanup_cmd,
+              **self._step_kwargs)
+      ])
+
+    return steps
+
diff --git a/cq/scripts/slave/recipe/lucicfg.py b/cq/scripts/slave/recipe/lucicfg.py
new file mode 100644
index 0000000..cfa5b37
--- /dev/null
+++ b/cq/scripts/slave/recipe/lucicfg.py
@@ -0,0 +1,23 @@
+"""Recipe for running lucicfg validation."""
+from slave import base_recipe
+from slave.step import lucicfg_step
+
+
+def GetValidBuildNames():
+  return ['lucicfg_validate']
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  del build_name
+  return LucicfgRecipe(**kwargs)
+
+
+class LucicfgRecipe(base_recipe.BaseRecipe):
+  """Recipe to run lucicfg validation."""
+
+  def get_steps(self):
+    return [
+      lucicfg_step.FindConfigsStep(**self._step_kwargs),
+      lucicfg_step.LucicfgValidateStep(**self._step_kwargs)
+    ]
+
diff --git a/cq/scripts/slave/recipe/ml_framework_sanitizer.py b/cq/scripts/slave/recipe/ml_framework_sanitizer.py
new file mode 100644
index 0000000..2b55547
--- /dev/null
+++ b/cq/scripts/slave/recipe/ml_framework_sanitizer.py
@@ -0,0 +1,105 @@
+"""Recipe for running asan on ml_framwork x86 unittests."""
+import os
+
+from slave import base_recipe
+from slave.step import gn_step
+from slave.step import ninja_step
+from slave.step import sanitizer_unittest_step
+
+BUILD_CONFIGS = {
+    'ml_framework_x64_asan': {
+        'sanitizer': 'asan',
+        'build_args_product' : 'asan_x64_eng',
+        'build_args' : 'enable_ml_framework=true is_debug=true '
+                       'chromecast_branding="google"',
+        'runtime_flags' : '',
+    },
+    'ml_framework_x64_tsan': {
+        'sanitizer': 'tsan',
+        'build_args_product' : 'tsan_x64_eng',
+        'build_args' : 'enable_ml_framework=true '
+                       'chromecast_branding="google"',
+        'runtime_flags' : '',
+    },
+    'ml_framework_x64_msan': {
+        'sanitizer': 'msan',
+        'build_args_product' : 'msan_x64_eng',
+        'build_args' : 'enable_ml_framework=true '
+                       'chromecast_branding="google"',
+        'runtime_flags' : '',
+    },
+    'ml_framework_x64_ubsan': {
+        'sanitizer': 'ubsan',
+        'build_args_product' : 'ubsan_x64',
+        'build_args' : 'enable_ml_framework=true',
+        'runtime_flags' : ' --single-process-tests',
+    },
+}
+
+
+def GetValidBuildNames():
+  return list(BUILD_CONFIGS.keys())
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  config = BUILD_CONFIGS[build_name]
+  return MlFrameworkAsanRecipe(build_name, config, **kwargs)
+
+
+class MlFrameworkAsanRecipe(base_recipe.BaseRecipe):
+  """This recipe runs the ml_framework unittests with ASAN/LSAN enabled."""
+  def  __init__(self, build_name, config, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._config = config
+    self._build_name = build_name
+
+  def get_steps(self):
+    steps = []
+    cwd = os.getcwd()
+    setup_steps = self.get_setup_steps()
+    chromium_root = setup_steps[0].get_project_path('chromium/src')
+    out_dir = os.path.join(
+        chromium_root,
+        'out_{}'.format(self._build_name),
+        'release')
+
+    gn_gen_step = gn_step.GnGenStep(
+        build_args_product=self._config['build_args_product'],
+        build_args=[self._config['build_args']],
+        out_dir=out_dir,
+        halt_on_failure=True,
+        cwd=cwd,
+        **self._step_kwargs
+    )
+    steps.append(gn_gen_step)
+    gn_args_step = gn_step.GnArgsStep(
+        out_dir=out_dir,
+        cwd=cwd,
+        halt_on_failure=True,
+        **self._step_kwargs
+    )
+    steps.append(gn_args_step)
+
+    ninja_build_step = ninja_step.NinjaStep(
+        out_dir=out_dir,
+        target='ml_framework_unittests',
+        cwd=cwd,
+        jobs=64,
+        halt_on_failure=True,
+        **self._step_kwargs
+    )
+    steps.append(ninja_build_step)
+
+    kwargs = self._step_kwargs
+    kwargs['unittest_prefix_filter'] = 'ml_'
+    sanitizer_test_step = sanitizer_unittest_step.SanitizerUnitTestShellStep(
+        name='ml_framework_unittests',
+        command=['./ml_framework_unittests' + self._config['runtime_flags']],
+        directory=out_dir,
+        sanitizer=self._config['sanitizer'],
+        halt_on_failure=True,
+        **kwargs
+    )
+    steps.append(sanitizer_test_step)
+    return steps
diff --git a/cq/scripts/slave/recipe/null.py b/cq/scripts/slave/recipe/null.py
new file mode 100644
index 0000000..44fe16e
--- /dev/null
+++ b/cq/scripts/slave/recipe/null.py
@@ -0,0 +1,19 @@
+"""
+Awesome recipe, that does nothing.
+"""
+
+from slave import base_recipe
+
+def GetValidBuildNames():
+  return ['null', 'vaapp-test']
+
+def CreateRecipe(build_name: str, **kwargs):
+  return NullRecipe(**kwargs)
+
+class NullRecipe(base_recipe.BaseRecipe):
+  def __init__(self, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=False, **kwargs)
+
+  def get_steps(self):
+    return []
\ No newline at end of file
diff --git a/cq/scripts/slave/recipe/ota_builder.py b/cq/scripts/slave/recipe/ota_builder.py
new file mode 100644
index 0000000..3be023c
--- /dev/null
+++ b/cq/scripts/slave/recipe/ota_builder.py
@@ -0,0 +1,1189 @@
+"""Recipe for running a ota build on the given patch."""
+import os
+
+from helpers import branch_utils
+from helpers import gcs_utils
+from helpers import repo_utils
+
+from slave import base_recipe
+from slave.step import gcs_upload_step
+from slave.step import gn_check_step
+from slave.step import host_tests_step
+from slave.step import landmine_step
+from slave.step import make_clean_step
+from slave.step import ota_build_tests_step
+from slave.step import ota_factory_tool_step
+from slave.step import ota_fct_step
+from slave.step import ota_oss_step
+from slave.step import ota_resign_step
+from slave.step import ota_step
+from slave.step import ota_strip_binaries_step
+from slave.step import partner_build_check_step
+from slave.test import test_generator
+
+PHOENIX_SCHEMA_BUILDS = [
+    'elaine-eng',  # 1.48_efct
+    'elaine-user',  # 1.48_efct
+    'gq-eng',  # 1.49qfsi
+    'gq-user',  # 1.49qfsi
+    'newman-eng',  # 1.38nf
+    'newman-user',  # 1.38nf
+    'nq-eng',  # 1.49qfsi
+    'nq-user',  # 1.49qfsi
+    'valens-eng',  # 1.41
+    'valens-user',  # 1.41
+    'vento-eng',  # 1.40fsi
+    'vento-user',  # 1.40fsi
+]
+
+BUILD_CONFIGS = {
+    'anchovy-eng': {
+        'build_flavor':
+            'release',
+        'resign':
+            ota_resign_step.EUREKA_RESIGN,
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'anchovy-user': {
+        'build_flavor':
+            'release',
+        'resign':
+            ota_resign_step.EUREKA_RESIGN,
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'assistantdefault-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ASSISTANTDEFAULT_RESIGN,
+        'partner_bucket': ['cast-partner-gva'],
+        'upload_libassisant_arch': 'arm',
+    },
+    'assistantdefault-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ASSISTANTDEFAULT_RESIGN,
+        'partner_bucket': ['cast-partner-gva'],
+        'upload_libassisant_arch': 'arm',
+    },
+    'assistantdefault-1led-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ASSISTANTDEFAULT_RESIGN,
+        'partner_bucket': ['cast-partner-gva'],
+    },
+    'assistantdefault-1led-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ASSISTANTDEFAULT_RESIGN,
+        'partner_bucket': ['cast-partner-gva'],
+    },
+    'assistantdefault-partner-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ASSISTANTDEFAULT_RESIGN,
+        'partner_bucket': ['cast-partner-gva'],
+        'upload_libassisant_arch': 'arm',
+    },
+    'audiodefault-eng': {
+        'build_flavor': 'release',
+        'partner_bucket': ['cast-partner-gva'],
+    },
+    'audiodefault-partner-eng': {
+        'build_flavor': 'release',
+        'partner_bucket': ['cast-partner-gva'],
+    },
+    'audiodefault-user': {
+        'build_flavor': 'release',
+        'partner_bucket': ['cast-partner-gva'],
+    },
+    'audiodefault_dynamic-eng': {
+        'build_flavor': 'release',
+        'partner_bucket': ['cast-partner-gva'],
+    },
+    'audiodefault_dynamic-user': {
+        'build_flavor': 'release',
+        'partner_bucket': ['cast-partner-gva'],
+    },
+    'biggie-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.BIGGIE_RESIGN,
+        'factory': ota_resign_step.BIGGIE_FACTORY,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+        'upload_libassisant_arch': 'arm64',
+    },
+    'biggie-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.BIGGIE_RESIGN,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+        'upload_libassisant_arch': 'arm64',
+        'branch_extra_params': {
+            '1.40rd': ['RETAIL_DEMO_BUILD=true'],
+        },
+    },
+    'bowie-eng': {
+        'build_flavor':
+            'release',
+        'board_name':
+            'bowie-p0',
+        'resign':
+            ota_resign_step.BOWIE_RESIGN,
+        'factory':
+            ota_resign_step.BOWIE_FACTORY,
+        'usbstick':
+            ota_resign_step.BOWIE_USBSTICK,
+        'test_board_name':
+            'bowie-p0',
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'bowie-user': {
+        'build_flavor':
+            'release',
+        'board_name':
+            'bowie-p0',
+        'resign':
+            ota_resign_step.BOWIE_RESIGN,
+        'test_board_name':
+            'bowie-p0',
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'brezza-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.BREZZA_RESIGN,
+        'factory': ota_resign_step.BREZZA_FACTORY,
+        'factory_tool': ota_resign_step.BREZZA_FACTORY_TOOL,
+        'usbstick': ota_resign_step.BREZZA_USBSTICK,
+        'partner_bucket': ['cast-partner-qualcomm-internal'],
+        'skip_ota_tests': True,
+    },
+    'brezza-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.BREZZA_RESIGN,
+        'partner_bucket': ['cast-partner-qualcomm-internal'],
+        'skip_ota_tests': True,
+    },
+    'brezza-partner-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.BREZZA_RESIGN,
+        'partner_bucket': ['cast-partner-qualcomm-internal'],
+        'branch_extra_params': {
+            'master': ['PARTNER_BUILD=true'],
+        },
+        'skip_ota_tests': True,
+    },
+    'caprica-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.CAPRICA_RESIGN,
+        'factory': ota_resign_step.CAPRICA_FACTORY,
+        'usbstick': ota_resign_step.CAPRICA_USBSTICK,
+        'partner_bucket': ['cast-partner-synaptics-internal'],
+    },
+    'caprica-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.CAPRICA_RESIGN,
+        'partner_bucket': ['cast-partner-synaptics-internal'],
+    },
+    'caprica-yeti-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.CAPRICA_RESIGN,
+        'factory': ota_resign_step.CAPRICA_FACTORY,
+    },
+    'caprica-yeti-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.CAPRICA_RESIGN,
+    },
+    'chorizo-eng': {
+        'build_flavor':
+            'release',
+        'resign':
+            ota_resign_step.CHORIZO_RESIGN,
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'chorizo-user': {
+        'build_flavor':
+            'release',
+        'max_ota_size':
+            104000000,
+        'resign':
+            ota_resign_step.CHORIZO_RESIGN,
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'chromeos_armv7a-eng': {
+        'build_flavor': 'release',
+        'board_name': 'sirocco-b1',
+        'build_usonia_daemon': True,
+        'build_thread_telemetry_service_cros': True,
+        'target_name': 'sirocco-eng',
+        'skip_ota_tests': True,
+    },
+    'chromeos_armv7a-user': {
+        'build_flavor': 'release',
+        'board_name': 'sirocco-b1',
+        'build_usonia_daemon': True,
+        'build_thread_telemetry_service_cros': True,
+        'target_name': 'sirocco-user',
+        'skip_ota_tests': True,
+    },
+    'core_runtime-eng': {
+        'build_flavor': 'release',
+        'partner_bucket': ['core-runtime-cb-upload'],
+    },
+    'core_runtime-user': {
+        'build_flavor': 'release',
+        'partner_bucket': ['core-runtime-cb-upload'],
+    },
+    'dragonfly-eng': {
+        'build_flavor': 'release',
+        'partner_bucket': ['cast-partner-sigmadesign-tv'],
+    },
+    'dragonfly-user': {
+        'build_flavor': 'release',
+        'partner_bucket': ['cast-partner-sigmadesign-tv'],
+    },
+    'elaine-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ELAINE_RESIGN,
+        'factory': ota_resign_step.ELAINE_FACTORY,
+        'usbstick': ota_resign_step.ELAINE_USBSTICK,
+        'clean_steps': [],
+        'partner_bucket': [
+            'cast-partner-amlogic-internal', 'cast-partner-broadcom-internal'
+        ],
+        'eng_build': True,
+    },
+    'elaine-user': {
+        'build_flavor':
+            'release',
+        'resign':
+            ota_resign_step.ELAINE_RESIGN,
+        'clean_steps': [],
+        'partner_bucket': [
+            'cast-partner-amlogic-internal', 'cast-partner-broadcom-internal'
+        ],
+    },
+    'elaine-fuchsia-eng': {
+        'build_flavor': 'release',
+        'board_name': 'elaine-b3',
+        'resign': ota_resign_step.ELAINE_FUCHSIA_RESIGN,
+        'clean_steps': [],
+        'default_properties': {
+            'fuchsia_sync': True,
+        },
+        'eng_build': True,
+    },
+    'elaine-fuchsia-user': {
+        'build_flavor': 'release',
+        'board_name': 'elaine-b3',
+        'resign': ota_resign_step.ELAINE_FUCHSIA_RESIGN,
+        'clean_steps': [],
+        'default_properties': {
+            'fuchsia_sync': True,
+        },
+    },
+    'estelle-eng': {
+        'build_flavor':
+            'release',
+        'resign':
+            ota_resign_step.ESTELLE_RESIGN,
+        'factory':
+            ota_resign_step.ESTELLE_FACTORY,
+        'usbstick':
+            ota_resign_step.ESTELLE_USBSTICK,
+        'partner_bucket': [
+            'cast-partner-amlogic-internal', 'cast-partner-broadcom-internal'
+        ],
+    },
+    'estelle-health-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ESTELLE_RESIGN,
+        'factory': ota_resign_step.ESTELLE_FACTORY,
+    },
+    'estelle-health-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ESTELLE_RESIGN,
+    },
+    'estelle-ml_framework-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ESTELLE_RESIGN,
+    },
+    'estelle-opal-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ESTELLE_RESIGN,
+        'factory': ota_resign_step.ESTELLE_FACTORY,
+    },
+    'estelle-opal-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ESTELLE_RESIGN,
+    },
+    'estelle-fuchsia-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ESTELLE_FUCHSIA_RESIGN,
+        'default_properties': {
+            'fuchsia_sync': True,
+        },
+    },
+    'estelle-fuchsia-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ESTELLE_FUCHSIA_RESIGN,
+        'default_properties': {
+            'fuchsia_sync': True,
+        },
+    },
+    'estelle-sdm-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ESTELLE_RESIGN,
+        'factory': ota_resign_step.ESTELLE_FACTORY,
+    },
+    'estelle-sdm-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.ESTELLE_RESIGN,
+    },
+    'estelle-user': {
+        'build_flavor':
+            'release',
+        'max_ota_size':
+            170000000,
+        'resign':
+            ota_resign_step.ESTELLE_RESIGN,
+        'partner_bucket': [
+            'cast-partner-amlogic-internal', 'cast-partner-broadcom-internal'
+        ],
+    },
+    'korlan-eng': {
+        'build_flavor': 'release',
+        'board_name': 'korlan-b1',
+        'max_ota_size': 43000000,
+        'resign': ota_resign_step.KORLAN_RESIGN,
+        'factory': ota_resign_step.KORLAN_FACTORY,
+        'usbstick': ota_resign_step.KORLAN_USBSTICK,
+        'factory_tool': ota_resign_step.KORLAN_FACTORY_TOOL,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+    },
+    'korlan-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.KORLAN_RESIGN,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+    },
+    'lumia-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.LUMIA_RESIGN,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+    },
+    'lumia-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.LUMIA_RESIGN,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+    },
+    'gq-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.GQ_RESIGN,
+        'factory': ota_resign_step.GQ_FACTORY,
+        'usbstick': ota_resign_step.GQ_USBSTICK,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+        'branch_extra_params': {
+            '1.49qfsi_retail': ['RETAIL_DEMO_BUILD=true'],
+        },
+        'check_camera_targets': True,
+        'eng_build': True,
+    },
+    'gq-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.GQ_RESIGN,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+        'branch_extra_params': {
+            '1.49qfsi_retail': ['RETAIL_DEMO_BUILD=true'],
+        },
+        'check_camera_targets': True,
+    },
+    'mozart-eng': {
+        'build_flavor': 'release',
+        'partner_bucket': ['cast-partner-mediatek-audio'],
+    },
+    'mushroom-eng': {
+        'build_flavor': 'release',
+        'max_ota_size': 115000000,
+        'resign': ota_resign_step.MUSHROOM_RESIGN,
+        'factory': ota_resign_step.MUSHROOM_FACTORY,
+        'usbstick': ota_resign_step.MUSHROOM_USBSTICK,
+        'partner_bucket': ['cast-partner-synaptics-internal'],
+    },
+    'mushroom-sdm-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.MUSHROOM_RESIGN,
+        'factory': ota_resign_step.MUSHROOM_FACTORY,
+    },
+    'mushroom-sdm-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.MUSHROOM_RESIGN,
+    },
+    'mushroom-user': {
+        'build_flavor': 'release',
+        'max_ota_size': 115000000,
+        'resign': ota_resign_step.MUSHROOM_RESIGN,
+        'partner_bucket': ['cast-partner-synaptics-internal'],
+        'branch_extra_params': {
+            '1.40rd': ['RETAIL_DEMO_BUILD=true'],
+        },
+    },
+    'newman-eng': {
+        'build_flavor': 'release',
+        'board_name': 'newman-b3',
+        'resign': ota_resign_step.NEWMAN_RESIGN_ENG,
+        'factory': ota_resign_step.NEWMAN_FACTORY,
+        'usbstick': ota_resign_step.NEWMAN_USBSTICK,
+        'partner_bucket': [
+            'cast-partner-amlogic-internal', 'cast-partner-broadcom-internal'
+        ],
+        'test_board_name': 'newman-b3',
+    },
+    'newman-eng_legacy': {
+        'build_flavor': 'release',
+        'board_name': 'newman-b3',
+        'resign': ota_resign_step.NEWMAN_RESIGN_ENG,
+        'factory': ota_resign_step.NEWMAN_FACTORY,
+        'partner_bucket': [
+            'cast-partner-amlogic-internal', 'cast-partner-broadcom-internal'
+        ],
+        'test_board_name': 'newman-b3',
+    },
+    'newman-fuchsia-eng': {
+        'build_flavor': 'release',
+        'board_name': 'newman-b3',
+        'resign': ota_resign_step.NEWMAN_FUCHSIA_RESIGN,
+        'default_properties': {
+            'fuchsia_sync': True,
+        },
+        'test_board_name': 'newman-b3',
+    },
+    'newman-fuchsia-user': {
+        'build_flavor': 'release',
+        'board_name': 'newman-b3',
+        'resign': ota_resign_step.NEWMAN_FUCHSIA_RESIGN,
+        'default_properties': {
+            'fuchsia_sync': True,
+        },
+        'test_board_name': 'newman-b3',
+    },
+    'newman-fuchsia_rev-eng': {
+        'build_flavor': 'release',
+        'board_name': 'newman-b3',
+        'resign': ota_resign_step.NEWMAN_FUCHSIA_RESIGN,
+        'default_properties': {
+            'fuchsia_sync': True,
+        },
+        'test_board_name': 'newman-b3',
+    },
+    'newman-fuchsia_rev-user': {
+        'build_flavor': 'release',
+        'board_name': 'newman-b3',
+        'resign': ota_resign_step.NEWMAN_FUCHSIA_RESIGN,
+        'default_properties': {
+            'fuchsia_sync': True,
+        },
+        'test_board_name': 'newman-b3',
+    },
+    'newman-user': {
+        'build_flavor': 'release',
+        'board_name': 'newman-b3',
+        'resign': ota_resign_step.NEWMAN_RESIGN,
+        'partner_bucket': [
+            'cast-partner-amlogic-internal', 'cast-partner-broadcom-internal'
+        ],
+        'test_board_name': 'newman-b3',
+    },
+    'newman-modular_comms-user': {
+        'build_flavor': 'release',
+        'board_name': 'newman-b3',
+        'resign': ota_resign_step.NEWMAN_RESIGN,
+        'test_board_name': 'newman-b3',
+    },
+    'nq-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.NQ_RESIGN,
+        'factory': ota_resign_step.NQ_FACTORY,
+        'usbstick': ota_resign_step.NQ_USBSTICK,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+        'check_camera_targets': True,
+        'eng_build': True,
+    },
+    'nq-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.NQ_RESIGN,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+        'check_camera_targets': True,
+    },
+    'np8340-eng': {
+        'build_flavor': 'release',
+    },
+    'np8340-user': {
+        'build_flavor': 'release',
+    },
+    'pepperoni-eng': {
+        'build_flavor':
+            'release',
+        'resign':
+            ota_resign_step.PEPPERONI_RESIGN,
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'pepperoni-user': {
+        'build_flavor':
+            'release',
+        'resign':
+            ota_resign_step.PEPPERONI_RESIGN,
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'pineapple-eng': {
+        'build_flavor':
+            'release',
+        'max_ota_size':
+            115000000,
+        'resign':
+            ota_resign_step.PINEAPPLE_RESIGN,
+        'factory':
+            ota_resign_step.PINEAPPLE_FACTORY,
+        'usbstick':
+            ota_resign_step.PINEAPPLE_USBSTICK,
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'pineapple-user': {
+        'build_flavor': 'release',
+        'max_ota_size': 115000000,
+        'resign': ota_resign_step.PINEAPPLE_RESIGN,
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+        'branch_extra_params': {
+            '1.40rd': ['RETAIL_DEMO_BUILD=true'],
+        },
+    },
+    'pineapple-sdm-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.PINEAPPLE_RESIGN,
+        'factory': ota_resign_step.PINEAPPLE_FACTORY,
+    },
+    'pineapple-sdm-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.PINEAPPLE_RESIGN,
+    },
+    'prince-demo-eng': {
+        'build_flavor': 'release',
+        'board_name': 'prince-p2',
+        'resign': ota_resign_step.PRINCE_RESIGN,
+        'test_board_name': 'prince-p2',
+    },
+    'prince-demo-user': {
+        'build_flavor': 'release',
+        'board_name': 'prince-p2',
+        'resign': ota_resign_step.PRINCE_RESIGN,
+        'test_board_name': 'prince-p2',
+    },
+    'prince-iot_cast-eng': {
+        'build_flavor':
+            'release',
+        'board_name':
+            'prince-p2',
+        'resign':
+            ota_resign_step.PRINCE_RESIGN,
+        'factory':
+            ota_resign_step.PRINCE_FACTORY,
+        'usbstick':
+            ota_resign_step.PRINCE_USBSTICK,
+        'factory_tool':
+            ota_resign_step.PRINCE_FACTORY_TOOL,
+        'test_board_name':
+            'prince-p2',
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'prince-eng': {
+        'build_flavor':
+            'release',
+        'board_name':
+            'prince-p2',
+        'resign':
+            ota_resign_step.PRINCE_RESIGN,
+        'factory':
+            ota_resign_step.PRINCE_FACTORY,
+        'usbstick':
+            ota_resign_step.PRINCE_USBSTICK,
+        'factory_tool':
+            ota_resign_step.PRINCE_FACTORY_TOOL,
+        'test_board_name':
+            'prince-p2',
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'prince-user': {
+        'build_flavor':
+            'release',
+        'board_name':
+            'prince-p2',
+        'resign':
+            ota_resign_step.PRINCE_RESIGN,
+        'test_board_name':
+            'prince-p2',
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'salami-eng': {
+        'build_flavor':
+            'release',
+        'resign':
+            ota_resign_step.SALAMI_RESIGN,
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'salami-user': {
+        'build_flavor':
+            'release',
+        'resign':
+            ota_resign_step.SALAMI_RESIGN,
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'sirocco-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.SIROCCO_RESIGN,
+        'factory': ota_resign_step.SIROCCO_FACTORY,
+        'usbstick': ota_resign_step.SIROCCO_USBSTICK,
+        'factory_tool': ota_resign_step.SIROCCO_FACTORY_TOOL,
+        'partner_bucket': ['cast-partner-qualcomm-internal'],
+        'skip_ota_tests': True,
+    },
+    'sirocco-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.SIROCCO_RESIGN,
+        'partner_bucket': ['cast-partner-qualcomm-internal'],
+        'skip_ota_tests': True,
+    },
+    'sirocco-partner-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.SIROCCO_RESIGN,
+        'partner_bucket': ['cast-partner-qualcomm-internal'],
+        'branch_extra_params': {
+            'master': ['PARTNER_BUILD=true'],
+        },
+        'skip_ota_tests': True,
+    },
+    'spencer-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.SPENCER_RESIGN,
+        'factory': ota_resign_step.SPENCER_FACTORY,
+        'usbstick': ota_resign_step.SPENCER_USBSTICK,
+        'factory_tool': ota_resign_step.SPENCER_FACTORY_TOOL,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+        'check_camera_targets': True,
+        'eng_build': True,
+    },
+    'spencer-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.SPENCER_RESIGN,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+        'check_camera_targets': True,
+    },
+    'a049-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.A049_RESIGN,
+        'factory': ota_resign_step.A049_FACTORY,
+        'usbstick': ota_resign_step.A049_USBSTICK,
+        'factory_tool': ota_resign_step.A049_FACTORY_TOOL,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+        'check_camera_targets': True,
+        'eng_build': True,
+    },
+    'a049-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.A049_RESIGN,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+        'check_camera_targets': True,
+    },
+    'sprinkles-eng': {
+        'build_flavor': 'release',
+        'board_name': 'sprinkles-korlan',
+        'max_ota_size': 43000000,
+        'resign': ota_resign_step.SPRINKLES_RESIGN,
+        'factory': ota_resign_step.SPRINKLES_FACTORY,
+        'usbstick': ota_resign_step.SPRINKLES_USBSTICK,
+        'factory_tool': ota_resign_step.SPRINKLES_FACTORY_TOOL,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+    },
+    'sprinkles-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.SPRINKLES_RESIGN,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+    },
+    'steak-eng': {
+        'build_flavor':
+            'release',
+        'resign':
+            ota_resign_step.STEAK_RESIGN,
+        'factory':
+            ota_resign_step.STEAK_FACTORY,
+        'usbstick':
+            ota_resign_step.STEAK_USBSTICK,
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'steak-user': {
+        'build_flavor':
+            'release',
+        'max_ota_size':
+            104000000,
+        'resign':
+            ota_resign_step.STEAK_RESIGN,
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'steak-yeti-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.STEAK_RESIGN,
+        'factory': ota_resign_step.STEAK_FACTORY,
+    },
+    'steak-yeti-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.STEAK_RESIGN,
+    },
+    'tvdefault-eng': {
+        'build_flavor': 'release',
+    },
+    'tvdefault-user': {
+        'build_flavor': 'release',
+    },
+    'valens-eng': {
+        'build_flavor':
+            'release',
+        'board_name':
+            'valens-b1',
+        'max_ota_size':
+            155000000,
+        'resign':
+            ota_resign_step.VALENS_RESIGN,
+        'factory':
+            ota_resign_step.VALENS_FACTORY,
+        'unand':
+            ota_resign_step.VALENS_UNAND,
+        'usbstick':
+            ota_resign_step.VALENS_USBSTICK,
+        'test_board_name':
+            'valens-b1',
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'valens-modular_comms-eng': {
+        'build_flavor': 'release',
+        'board_name': 'valens-b1',
+        'max_ota_size': 155000000,
+        'resign': ota_resign_step.VALENS_RESIGN,
+        'test_board_name': 'valens-b1',
+    },
+    'valens-user': {
+        'build_flavor':
+            'release',
+        'board_name':
+            'valens-b1',
+        'max_ota_size':
+            130000000,
+        'resign':
+            ota_resign_step.VALENS_RESIGN,
+        'test_board_name':
+            'valens-b1',
+        'partner_bucket': [
+            'cast-partner-marvell-internal',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'vento-eng': {
+        'build_flavor':
+            'release',
+        'board_name':
+            'vento-b3',
+        'max_ota_size':
+            158000000,
+        'factory':
+            ota_resign_step.VENTO_FACTORY,
+        'unand':
+            ota_resign_step.VENTO_UNAND,
+        'resign':
+            ota_resign_step.VENTO_RESIGN,
+        'test_board_name':
+            'vento-b3',
+        'partner_bucket': [
+            'cast-partner-qca-wireless',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'vento-user': {
+        'build_flavor':
+            'release',
+        'board_name':
+            'vento-b3',
+        'max_ota_size':
+            133000000,
+        'resign':
+            ota_resign_step.VENTO_RESIGN,
+        'test_board_name':
+            'vento-b3',
+        'partner_bucket': [
+            'cast-partner-qca-wireless',
+            'cast-partner-synaptics-internal',
+        ],
+    },
+    'venus-eng': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.VENUS_RESIGN,
+        'factory': ota_resign_step.VENUS_FACTORY,
+        'factory_tool': ota_resign_step.VENUS_FACTORY_TOOL,
+        'usbstick': ota_resign_step.VENUS_USBSTICK,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+        'check_camera_targets': True,
+        'eng_build': True,
+    },
+    'venus-user': {
+        'build_flavor': 'release',
+        'resign': ota_resign_step.VENUS_RESIGN,
+        'partner_bucket': ['cast-partner-amlogic-internal'],
+        'check_camera_targets': True,
+    },
+}
+
+SKIP_OTA_TESTS_BRANCH = [
+    '1.55sfsi',
+    '1.57q',
+]
+
+for phoenix_schema_build in PHOENIX_SCHEMA_BUILDS:
+  BUILD_CONFIGS[phoenix_schema_build.replace('-', ota_step.GANETI_TAG)] = (
+      BUILD_CONFIGS[phoenix_schema_build])
+
+# TODO(b/117437843) Remove prod after migtation completion
+PARTNER_ARCHIVE_BUCKET_STAGING_BUCKET = {
+    'prod': 'cast-partner-staging',
+    'staging': 'cast-partner-staging',
+    'dev': 'cast-partner-dev',
+}
+
+PARTNER_BUILDS = {'audiodefault-eng'}
+
+OSS_SUPPORT_MIN_BRANCH = '1.10'
+OSS_SUPPORT_BUILDER_PREFIX_WHITELIST = 'combined-sdk-kernel-bootloader'
+
+FACTORY_SUPPORT_MIN_BRANCH = '1.21'
+FACTORY_TOOL_SUPPORT_MIN_BRANCH = '1.37'
+
+CAMERA_TARGET_CHECK_MIN_BRANCH = '1.57'
+
+USONIA_DAEMON_CHECK_MIN_BRANCH = '1.61'
+THREAD_TELEMETRY_SERVICE_CROS_CHECK_MIN_BRANCH = '1.61'
+
+
+def GetValidBuildNames():
+  return list(BUILD_CONFIGS.keys())
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  return OtaRecipe(build_name, BUILD_CONFIGS[build_name], **kwargs)
+
+
+class OtaRecipe(base_recipe.BaseRecipe):
+  """Recipe to run an ota build."""
+
+  def __init__(self, build_name, build_config, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._build_name = build_name
+    self._build_config = build_config
+    # Extracts default_properties from build config and adds it to the overall
+    # properties.
+    default_properties = build_config.get('default_properties', {})
+    self._step_kwargs['properties'] = dict(
+        list(default_properties.items()) +
+        list(self._step_kwargs.get('properties', {}).items()))
+    self._skip_landmine = self.build_system == 'catabuilder'
+    self._skip_gncheck = self.build_system == 'catabuilder'
+
+  def _oss_run_condition(self):
+    if self.build_system != 'catabuilder':
+      return False
+    if self._builds_usonia_daemon():
+      return False
+    if self._build_name.startswith(OSS_SUPPORT_BUILDER_PREFIX_WHITELIST):
+      return True
+    if not self._build_name.endswith('-user'):
+      return False
+
+    # Here we check whether the existing branch is > the
+    # OSS_SUPPORT_MIN_BRANCH which has support for OSS on the
+    # dev side.  If there is non-dot-release branch or we are less
+    # than this supported branch, trivially return.
+    return (branch_utils.is_release_branch(self._manifest_branch) and
+            branch_utils.is_branch_equal_to_or_later_than(
+                self._manifest_branch, OSS_SUPPORT_MIN_BRANCH))
+
+  def _fct_run_condition(self):
+    if 'factory' not in self._build_config:
+      return False
+    return branch_utils.is_branch_equal_to_or_later_than(
+        self._manifest_branch, FACTORY_SUPPORT_MIN_BRANCH)
+
+  def _usbstick_run_condition(self):
+    if 'usbstick' not in self._build_config:
+      return False
+    return branch_utils.is_branch_equal_to_or_later_than(
+        self._manifest_branch, FACTORY_SUPPORT_MIN_BRANCH)
+
+  def _factory_tool_run_condition(self):
+    if 'factory_tool' not in self._build_config:
+      return False
+    return branch_utils.is_branch_equal_to_or_later_than(
+        self._manifest_branch, FACTORY_TOOL_SUPPORT_MIN_BRANCH)
+
+  def _ota_extra_params(self):
+    return self._build_config.get(
+        'build_extra_params', []
+    ) + self._build_config.get('branch_extra_params', {}).get(
+        self._manifest_branch, []
+    )
+
+  def get_clean_steps(self):
+    """Builds a list of all clean steps that should be executed by this recipe.
+
+    Returns:
+      List of slave.base_step.BaseStep commands to be executed.
+    """
+    return [make_clean_step.MakeCleanStep(**self._step_kwargs)]
+
+  def _camera_target_check_condition(self):
+    return (BUILD_CONFIGS[self._build_name].get('check_camera_targets', False)
+            and branch_utils.is_branch_equal_to_or_later_than(
+                self._manifest_branch, CAMERA_TARGET_CHECK_MIN_BRANCH))
+
+  def _builds_usonia_daemon(self):
+    return (BUILD_CONFIGS[self._build_name].get('build_usonia_daemon', False)
+            and branch_utils.is_branch_equal_to_or_later_than(
+                self._manifest_branch, USONIA_DAEMON_CHECK_MIN_BRANCH))
+
+  def _builds_thread_telemetry_service_cros(self):
+    return (BUILD_CONFIGS[self._build_name].get(
+        'build_thread_telemetry_service_cros', False) and
+            branch_utils.is_branch_equal_to_or_later_than(
+                self._manifest_branch, USONIA_DAEMON_CHECK_MIN_BRANCH))
+
+  def get_steps(self):
+    steps = []
+
+    if not self._skip_landmine:
+      steps.append(landmine_step.LandmineStep(**self._step_kwargs))
+    if not self._skip_gncheck and not self._builds_usonia_daemon() \
+      and not self._builds_thread_telemetry_service_cros():
+      steps.append(
+          gn_check_step.GnCheckForOta(self._build_name, **self._step_kwargs))
+
+    board_name = self._build_config.get('board_name')
+    apply_dev_signing = self.build_system == 'catabuilder'
+    max_ota_size = self._build_config.get('max_ota_size')
+    target_name = self._build_config.get('target_name')
+    steps.append(
+        ota_step.OtaStep(
+            self._build_name,
+            board_name=board_name,
+            extra_params=self._ota_extra_params(),
+            halt_on_failure=True,
+            max_ota_size=max_ota_size,
+            target_name=target_name,
+            **self._step_kwargs))
+
+    if self._camera_target_check_condition():
+      product_name = self._build_name.split('-')[0]
+      is_eng_build = BUILD_CONFIGS[self._build_name].get('eng_build', False)
+      steps.append(
+          ota_step.OtaCameraTargetsStep(product_name, is_eng_build,
+                                        **self._step_kwargs))
+
+    if self._builds_usonia_daemon():
+      steps.append(
+          ota_step.OtaUsoniaDaemonStep(self._build_name, **self._step_kwargs))
+    if self._builds_thread_telemetry_service_cros():
+      steps.append(
+          ota_step.ThreadTelemetryServiceCrosStep(
+              build_name=self._build_name, **self._step_kwargs))
+
+    if self.build_system == 'catabuilder':
+      steps.append(ota_step.OtaPropStep(**self._step_kwargs))
+
+    if self._oss_run_condition():
+      steps.append(
+          ota_oss_step.OtaOssStep(
+              self._build_name, halt_on_failure=True, **self._step_kwargs))
+
+    # factory_tools have their own makefiles for compiling
+    if self._factory_tool_run_condition():
+      steps.append(
+          ota_factory_tool_step.CompileFactoryToolStep(
+              self._build_name,
+              self._build_config.get('factory_tool', {}),
+              halt_on_failure=True,
+              **self._step_kwargs))
+    if self._fct_run_condition():
+      steps.append(
+          ota_fct_step.OtaFctCompileStep(
+              self._build_name,
+              self._build_config.get('factory', {}),
+              halt_on_failure=True,
+              **self._step_kwargs))
+      # Generation of image/board specific FCT images performed by the same
+      # resign script but from different intermediate source binaries
+    steps.extend(
+        ota_resign_step.get_resign_steps(
+            self._build_name,
+            resign_config=self._build_config.get('factory'),
+            build_branch=self._manifest_branch,
+            apply_dev_signing=apply_dev_signing,
+            halt_on_failure=True,
+            **self._step_kwargs))
+    steps.extend(
+        ota_resign_step.get_resign_steps(
+            self._build_name,
+            resign_config=self._build_config.get('unand'),
+            build_branch=self._manifest_branch,
+            apply_dev_signing=apply_dev_signing,
+            halt_on_failure=True,
+            **self._step_kwargs))
+    if self._usbstick_run_condition():
+      steps.append(
+          ota_fct_step.OtaFctCompileStep(
+              self._build_name,
+              self._build_config.get('usbstick', {}),
+              name='build usbstick images',
+              halt_on_failure=True,
+              **self._step_kwargs))
+      steps.extend(
+          ota_resign_step.get_resign_steps(
+              self._build_name,
+              resign_config=self._build_config.get('usbstick'),
+              build_branch=self._manifest_branch,
+              apply_dev_signing=apply_dev_signing,
+              halt_on_failure=True,
+              **self._step_kwargs))
+    steps.extend(
+        ota_resign_step.get_resign_steps(
+            self._build_name,
+            resign_config=self._build_config.get('resign'),
+            build_branch=self._manifest_branch,
+            apply_dev_signing=apply_dev_signing,
+            halt_on_failure=True,
+            **self._step_kwargs))
+
+    if ((not self._build_config.get('skip_ota_tests')) and
+        (self._manifest_branch not in SKIP_OTA_TESTS_BRANCH)):
+      test_board_name = self._build_config.get('test_board_name')
+      steps.extend([
+          ota_build_tests_step.OtaBuildTestsStep(
+              self._build_name,
+              board_name=test_board_name,
+              extra_params=self._ota_extra_params(),
+              halt_on_failure=True,
+              **self._step_kwargs),
+          host_tests_step.RunHostTestsStep(**self._step_kwargs),
+          ota_strip_binaries_step.OtaStripBinariesStep(
+              halt_on_failure=True, **self._step_kwargs),
+      ])
+
+    # if this is a partner build, we need to do additional checks
+    if self._build_name in PARTNER_BUILDS:
+      build_flavor = self._build_config.get('build_flavor', 'release')
+      steps.append(
+          partner_build_check_step.PartnerBuildCheckStep(
+              self._build_name, build_flavor, **self._step_kwargs))
+
+    # TODO(b/118514002) remove once copy to partners service will be available
+    if self.build_system == 'catabuilder':
+      steps.extend(self._partner_upload_steps())
+
+    return steps
+
+  # TODO(b/118514002) remove once copy to partners service will be available
+  def _partner_upload_steps(self):
+    steps = []
+    gcs_path = self._get_partner_gcs_path()
+    gclient_out_path = '/tmp/{}_{}_gclient_out.json'.format(
+        self._build_name, self.build_number)
+
+    # TODO(b/117437843) Remove recipe_ check after migtation completion
+    for partner_bucket in self._build_config.get('partner_bucket', []):
+      build_env = self._properties.get('build_env', 'dev')
+      if build_env != 'prod' or self._build_name.startswith('recipe_'):
+        upload_bucket = PARTNER_ARCHIVE_BUCKET_STAGING_BUCKET[build_env]
+        upload_path = os.path.join(partner_bucket, gcs_path + '/')
+      else:
+        upload_bucket = partner_bucket
+        upload_path = gcs_path + '/'
+
+      # Upload gclient_out.json file
+      if os.path.isfile(gclient_out_path):
+        steps.append(
+            gcs_upload_step.GcsUploadStep(
+                upload_bucket,
+                upload_path,
+                name='gclient_out.json upload to {}'.format(partner_bucket),
+                src_path=gclient_out_path,
+                result_property='gcs_output_{}'.format(partner_bucket),
+                **self._step_kwargs))
+
+      steps.append(
+          gcs_upload_step.GcsUploadStep(
+              upload_bucket,
+              upload_path,
+              name='upload to {}'.format(partner_bucket),
+              result_property='gcs_output_{}'.format(partner_bucket),
+              **self._step_kwargs))
+
+    return steps
+
+  def _get_partner_gcs_path(self):
+    """Construct the gcs relative path within a bucket for specified build."""
+    gcs_path = [
+        self._properties['repo_alias'],
+        self._properties['manifest_branch'],
+        self._properties['buildername'],
+        self._properties['buildset'],
+    ]
+    return '/'.join(str(i) for i in gcs_path)
diff --git a/cq/scripts/slave/recipe/precommit.py b/cq/scripts/slave/recipe/precommit.py
new file mode 100644
index 0000000..c75b619
--- /dev/null
+++ b/cq/scripts/slave/recipe/precommit.py
@@ -0,0 +1,57 @@
+"""Recipe for running pre-commit tests.
+
+pre-commit is a framework written in python that allows each
+repository to define its own set of checks that will be run
+before code is committed. It uses a git hook and a config file
+at the root of the repository.
+
+This recipe looks for the config file and runs pre-commit.
+"""
+import logging
+import os
+
+from slave import base_recipe
+from slave.step import precommit_step
+from slave.step import py_env_setup_step
+
+CONFIG_FILE_DEFAULT = ".pre-commit-config.yaml"
+CONFIG_FILE_BETA = ".pre-commit-config.beta.yaml"
+
+def GetValidBuildNames():
+  return ["precommit", "precommit-beta"]
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Defines how this recipe gets built."""
+  if build_name.endswith("-beta"):
+    config_file = CONFIG_FILE_BETA
+    show_output = False
+  else:
+    config_file = CONFIG_FILE_DEFAULT
+    show_output = True
+  return PreCommitRecipe(
+    config_file=config_file,
+    show_output=show_output,
+    **kwargs)
+
+class PreCommitRecipe(base_recipe.BaseRecipe):
+  """Recipe to execute pre-commit hooks."""
+
+  def __init__(self, config_file, show_output, **kwargs):
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+    self._config_file = config_file
+    self._repo_root_directory = kwargs["eureka_root"]
+    self._show_output = show_output
+
+  def get_steps(self):
+    return [
+      py_env_setup_step.PyEnvSetupStep(
+        py3=True,
+        requirements_project="continuous-tests",
+        requirements_path="configs/pre-commit/requirements.txt",
+        **self._step_kwargs),
+      precommit_step.PreCommitStep(
+        config_file=self._config_file,
+        repo_root_directory=self._repo_root_directory,
+        show_output=self._show_output,
+        **self._step_kwargs),
+    ]
diff --git a/cq/scripts/slave/recipe/py_test_runner.py b/cq/scripts/slave/recipe/py_test_runner.py
new file mode 100644
index 0000000..9803a33
--- /dev/null
+++ b/cq/scripts/slave/recipe/py_test_runner.py
@@ -0,0 +1,155 @@
+"""Recipe for running python tests against the given build."""
+import logging
+import os
+from pathlib import Path
+from typing import Dict, List, Union
+
+from slave import base_recipe
+from slave.step import py_env_setup_step
+from slave.step import py_test_step
+
+
+LOGGER = logging.getLogger(__name__)
+DEFAULT_UNIT_TEST_TIMEOUT_SECONDS = 300
+DEFAULT_INTEGRATION_TEST_TIMEOUT_SECONDS = 3600
+DEFAULT_E2E_TEST_TIMEOUT_SECONDS = 3600
+UNIT_TEST_REGEX_SUFFIX = r'.*_test\.py$'
+INTEGRATION_TEST_REGEX_SUFFIX = r'.*_test_integration\.py$'
+E2E_TEST_REGEX_SUFFIX = r'.*_test_case.py$'
+PY3_EXCLUDED_PATH = [
+    'cq/buildbot-masters',
+    'cq/scripts/master',
+]
+
+
+BUILD_CONFIGS = {
+    'py_unittest_builder_masters': {
+        'project': 'builder/masters',
+        'py_requirements_path': 'requirements_buildbot.txt',
+        'py_requirements_project': 'builder/masters',
+        'py_packages_mirror_dir': 'python_packages',
+        'test_file_filter_regex': UNIT_TEST_REGEX_SUFFIX,
+        'test_timeout_seconds': DEFAULT_UNIT_TEST_TIMEOUT_SECONDS,
+    },
+    'py_unittest_builder_scripts': {
+        'project': 'builder/scripts',
+        'test_file_filter_regex': UNIT_TEST_REGEX_SUFFIX,
+        'test_timeout_seconds': DEFAULT_UNIT_TEST_TIMEOUT_SECONDS,
+    },
+    'py3_unittest_continuous_tests': {
+        'project': 'continuous-tests',
+        'py_requirements_path': 'cq/requirements_recipe3.txt',
+        'py_requirements_project': 'continuous-tests',
+        'py_packages_mirror_dir': 'python_packages',
+        'test_file_filter_regex': UNIT_TEST_REGEX_SUFFIX,
+        'test_timeout_seconds': DEFAULT_UNIT_TEST_TIMEOUT_SECONDS * 2,
+        'module_path_regex': '^((?!{}).)*$'.format('|'.join(PY3_EXCLUDED_PATH)),
+        'py3': True,
+    },
+    'py_unittest_test': {
+        'project': 'test',
+        'test_file_filter_regex': UNIT_TEST_REGEX_SUFFIX,
+        'test_timeout_seconds': DEFAULT_UNIT_TEST_TIMEOUT_SECONDS,
+    },
+    'py3_unittest_test': {
+        'project': 'test',
+        'test_file_filter_regex': UNIT_TEST_REGEX_SUFFIX,
+        'test_timeout_seconds': DEFAULT_UNIT_TEST_TIMEOUT_SECONDS * 2,
+        'py3': True,
+        'py_requirements_path': os.path.join('lab_system', 'requirements3.txt'),
+    },
+    'py_unittest_chromecast_internal': {
+        'project': 'chromecast/internal',
+        'py_requirements_path': os.path.join('lab_system', 'requirements3.txt'),
+        'py3': True,
+        # The gunittest runner filters use full, absolute file paths,
+        # and will search into the nested git projects within
+        # chromecast/internal. To avoid finding unwanted test modules, include
+        # a 'chromecast/internal' prefix and only search specific directories.
+        'test_file_filter_regex':
+            (r'cast/internal/(build|tools|android/build)/' +
+             UNIT_TEST_REGEX_SUFFIX),
+        'test_timeout_seconds': DEFAULT_UNIT_TEST_TIMEOUT_SECONDS,
+    },
+    'py_integration_test_chromecast_internal': {
+        'project': 'chromecast/internal',
+        'py_requirements_path': os.path.join('lab_system', 'requirements3.txt'),
+        'py3': True,
+        # The gunittest runner filters use full, absolute file paths,
+        # and will search into the nested git projects within
+        # chromecast/internal. To avoid finding unwanted test modules, include
+        # a 'chromecast/internal' prefix and only search specific directories.
+        'test_file_filter_regex':
+            (r'cast/internal/(build|tools|android/build)/' +
+             INTEGRATION_TEST_REGEX_SUFFIX),
+        'test_timeout_seconds': DEFAULT_INTEGRATION_TEST_TIMEOUT_SECONDS,
+    },
+}
+Config = Dict[str, Union[str, int]]
+
+def GetValidBuildNames():
+  return list(BUILD_CONFIGS.keys())
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  if build_name in BUILD_CONFIGS:
+    return PyTestRecipe(
+      config=BUILD_CONFIGS[build_name],
+      **kwargs)
+  raise base_recipe.BuildNameNotSupportedError(build_name)
+
+
+class PyTestRecipe(base_recipe.BaseRecipe):
+  """Recipe to run python tests on a repository."""
+
+  def __init__(self,
+               config: Config,
+               **kwargs):
+    super().__init__(**kwargs)
+    self._config = config
+    LOGGER.info("Eureka root when creatini PyTestRecipe: %s", self._eureka_root)
+
+  def get_steps(self):
+    requirements_project = self._config.get('py_requirements_project', 'test')
+    requirements_path = self._config.get(
+        'py_requirements_path', os.path.join('lab_system', 'requirements.txt'))
+    python_packages_dir = self._config.get(
+        'py_packages_mirror_dir', 'team-catatester/python_packages')
+    test_file_filter_regex = self._config.get(
+        'test_file_filter_regex', UNIT_TEST_REGEX_SUFFIX)
+    test_timeout_seconds = self._config.get(
+        'test_timeout_seconds', DEFAULT_UNIT_TEST_TIMEOUT_SECONDS)
+    test_py3 = self._config.get('py3', False)
+    module_path_regex = self._config.get('module_path_regex', '.')
+    LOGGER.info("Eureka root when creating additional import paths: %s", self._eureka_root)
+    additional_import_paths = [
+        self._eureka_root / path for path
+        in (
+            'builder/masters',
+            'builder/masters/src',
+            'chromium/src/third_party/catapult/telemetry',
+            'chromium/src/chromecast/internal',
+            'chromium/src/chromecast/tools',
+            'continuous-tests',
+            'continuous-tests/cq/scripts',
+            'test',
+            'testing/external/autotest',
+            'testing/external/chromite',
+            'testing/sprockets',
+         )]
+    return [
+        py_env_setup_step.PyEnvSetupStep(
+            additional_import_paths=additional_import_paths,
+            local_packages_dir=python_packages_dir,
+            py3=test_py3,
+            requirements_path=requirements_path,
+            requirements_project=requirements_project,
+            **self._step_kwargs),
+        py_test_step.PyTestStep(
+            project=self._config.get('project'),
+            test_file_filter_regex=test_file_filter_regex,
+            timeout=test_timeout_seconds,
+            module_path_regex=module_path_regex,
+            **self._step_kwargs)
+    ]
diff --git a/cq/scripts/slave/recipe/pylint.py b/cq/scripts/slave/recipe/pylint.py
new file mode 100644
index 0000000..9170b39
--- /dev/null
+++ b/cq/scripts/slave/recipe/pylint.py
@@ -0,0 +1,47 @@
+"""Recipe for running a pylint on the given build."""
+from slave import base_recipe
+from slave.step import pylint_step
+
+# Note that the keys are Gerrit project names.
+PROJECT_CONFIGS = {
+    'continuous-tests': {
+        'pylintrc_file': 'pylintrc',
+        'deny_list_filter': [
+            'cq/scripts/master/buildbucket/',
+            'cq/scripts/helper/cpplint/',
+            'cq/scripts/master/deferred_resource.py',
+            'cq/appengine/genfiles',
+        ],
+    },
+    'builder/buildbot': {
+        'deny_list_filter': ['.*'],
+    },
+    'test': {
+        'pylintrc_file': '.pylintrc',
+    }
+}
+
+
+def GetValidBuildNames():
+  return ['lint']
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  del build_name  # Unused.
+  return PylintRecipe(**kwargs)
+
+
+class PylintRecipe(base_recipe.BaseRecipe):
+  """Recipe to lint a repository."""
+
+  def get_steps(self):
+    deny_list_filter = PROJECT_CONFIGS.get(self._properties['patch_project'],
+                                           {}).get('deny_list_filter', None)
+    pylintrc_file = PROJECT_CONFIGS.get(self._properties['patch_project'],
+                                           {}).get('pylintrc_file', None)
+
+    return [
+        pylint_step.PylintStep(pylintrc_file=pylintrc_file,
+            deny_list_filter=deny_list_filter, **self._step_kwargs),
+    ]
diff --git a/cq/scripts/slave/recipe/sdk_ota_builder.py b/cq/scripts/slave/recipe/sdk_ota_builder.py
new file mode 100644
index 0000000..35fba35
--- /dev/null
+++ b/cq/scripts/slave/recipe/sdk_ota_builder.py
@@ -0,0 +1,56 @@
+"""Recipe for running a ota build on the given SDK patch."""
+from slave import base_recipe
+from slave.recipe import combined_sdk
+from slave.recipe import ota_builder
+from slave.step import combined_sdk_compile
+
+SDK_CONFIG = {
+    'korlan-eng-with-sdk': {
+        'product': 'korlan',
+        'sdk_path': 'vendor/amlogic/amlogic_sdk_korlan',
+        'fake_out_path': 'out',
+        'ota_config_key': 'korlan-eng',
+        'sdk_config_key': 'combined-sdk-kernel-bootloader-amlogic-korlan',
+    },
+    'korlan-user-with-sdk': {
+        'product': 'korlan',
+        'sdk_path': 'vendor/amlogic/amlogic_sdk_korlan',
+        'fake_out_path': 'out',
+        'ota_config_key': 'korlan-user',
+        'sdk_config_key': 'combined-sdk-kernel-bootloader-amlogic-korlan',
+    },
+}
+
+
+def GetValidBuildNames():
+  return list(SDK_CONFIG.keys())
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  return SdkOtaRecipe(build_name, **kwargs)
+
+
+class SdkOtaRecipe(base_recipe.BaseRecipe):
+  """Recipe to run an ota build."""
+
+  def __init__(self, build_name, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._build_name = build_name
+    self._build_config = SDK_CONFIG[self._build_name]
+    self._build_kwargs = kwargs
+
+  def get_steps(self):
+    steps = []
+
+    sdk_config_key = self._build_config.get('sdk_config_key')
+    sdk_path = self._build_config.get('sdk_path')
+    combined_sdk_recipe = combined_sdk.CreateRecipe(
+        sdk_config_key, cwd=sdk_path, repo_regex=sdk_path,
+        **self._build_kwargs)
+    steps += combined_sdk_recipe.get_steps_for_ota_builder()
+    ota_config_key = self._build_config.get('ota_config_key')
+    steps += ota_builder.CreateRecipe(ota_config_key,
+                                      **self._build_kwargs).get_steps()
+    return steps
diff --git a/cq/scripts/slave/recipe/sprockets_conformance_test_runner.py b/cq/scripts/slave/recipe/sprockets_conformance_test_runner.py
new file mode 100644
index 0000000..940c19f
--- /dev/null
+++ b/cq/scripts/slave/recipe/sprockets_conformance_test_runner.py
@@ -0,0 +1,49 @@
+"""Recipe for running a the Sprockets Conformance tests."""
+from slave import base_recipe
+from slave.step import cast_shell_step
+from slave.step import landmine_step
+from slave.step import sprockets_step
+
+_BUILD_ARGS_PRODUCT = 'video_x64_eng'
+_BUILD_ARGS_OFFICIAL = False
+_BUILD_BRANDING = 'chrome'
+_BUILD_ARCH = 'clang'
+_BUILD_PRODUCT = 'chromecast'
+# TODO(mbjorge|seantopping): Change to 'Release' when b/32749161 is resolved.
+_BUILD_FLAVOR = 'Debug'
+_ENABLED_CHIRP = False
+# TODO(b/24062312): 'clang' --> 'x86'
+
+
+def GetValidBuildNames():
+  return [
+      'sprockets_conformance_tests',
+  ]
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  del build_name  # Unused.
+  return SprocketsConformanceTestsRecipe(**kwargs)
+
+
+class SprocketsConformanceTestsRecipe(base_recipe.BaseRecipe):
+  """Recipe to run a Sprockets conformance tests against desktop cast shell."""
+
+  def __init__(self, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._properties = kwargs.get('properties', {})
+
+  def get_steps(self):
+    return [
+        landmine_step.LandmineStep(**self._step_kwargs),
+        cast_shell_step.CastShellStep(_BUILD_BRANDING, _BUILD_ARCH,
+                                      _BUILD_PRODUCT,
+                                      _BUILD_FLAVOR, _ENABLED_CHIRP,
+                                      build_args_product=_BUILD_ARGS_PRODUCT,
+                                      build_args_official=_BUILD_ARGS_OFFICIAL,
+                                      halt_on_failure=True,
+                                      **self._step_kwargs),
+        sprockets_step.SprocketsConformanceTestStep(**self._step_kwargs)
+    ]
diff --git a/cq/scripts/slave/recipe/x86_cast_camera_hal_builder.py b/cq/scripts/slave/recipe/x86_cast_camera_hal_builder.py
new file mode 100644
index 0000000..d79d853
--- /dev/null
+++ b/cq/scripts/slave/recipe/x86_cast_camera_hal_builder.py
@@ -0,0 +1,54 @@
+import os
+import pathlib
+
+from slave import base_recipe
+from slave.step import x86_cast_camera_hal_step
+from slave.step import code_coverage_step
+
+BUILD_CONFIGS = {
+    'cast_camera_hal_tests_x86_64': {
+        'code_coverage': True,
+    },
+    'cast_camera_hal_tests_x86_64_coverage': {
+        'code_coverage': True,
+    }
+}
+
+def GetValidBuildNames():
+  return list(BUILD_CONFIGS.keys())
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  config = BUILD_CONFIGS[build_name]
+  return CastCameraHalRecipe(config, **kwargs)
+
+
+class CastCameraHalRecipe(base_recipe.BaseRecipe):
+  """ Recipe for build/run cast camera hal test"""
+
+  def __init__(self, config, **kwargs):
+    self._config = config
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+
+  def get_steps(self):
+    steps = [
+        x86_cast_camera_hal_step.BuildCastCameraHalTestStep(
+            **self._step_kwargs),
+        x86_cast_camera_hal_step.RunCastCameraHalTestStep(
+            **self._step_kwargs)
+    ]
+
+    if self._config.get('code_coverage', False):
+      lcov_paths = [os.path.join(self._executor.get_gcs_dir(),
+                                 'code_coverage_html',
+                                 'coverage.lcov')]
+      steps += [
+          x86_cast_camera_hal_step.InstallCoverageTools(
+              **self._step_kwargs),
+          x86_cast_camera_hal_step.CollectCameraHalTestCoverageStep(
+              **self._step_kwargs),
+          code_coverage_step.CollectLcovCoverageStep(
+              lcov_paths, **self._step_kwargs),
+      ]
+
+    return steps
diff --git a/cq/scripts/slave/recipe/x86_integration_test_runner.py b/cq/scripts/slave/recipe/x86_integration_test_runner.py
new file mode 100644
index 0000000..8a85a54
--- /dev/null
+++ b/cq/scripts/slave/recipe/x86_integration_test_runner.py
@@ -0,0 +1,81 @@
+"""Recipe for running integration tests against locally started cast shell."""
+import os
+
+from slave import base_recipe
+from slave.step import cast_shell_step
+from slave.step import py_env_setup_step
+from slave.step import x86_integration_test_step
+
+# TODO(b/24062312): 'clang' --> 'x86'
+BUILD_ARCH = 'clang'
+BUILD_ARGS_PRODUCT = 'video_x64_eng'
+BUILD_ARGS_OFFICIAL = False
+BUILD_BRANDING = 'chrome'
+BUILD_FLAVOR = 'Debug'
+BUILD_PRODUCT = 'chromecast'
+ENABLE_CHIRP = False
+
+BUILD_CONFIGS = {
+  'cast_compliance_tests': {
+      'py3': True,
+      'py_requirements_path': os.path.join('lab_system', 'requirements3.txt'),
+  },
+  'cast_compliance_tests_rbe': {}
+}
+
+
+def GetValidBuildNames():
+  return BUILD_CONFIGS.keys()
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  return CastComplianceTestRecipe(build_name,
+                                  BUILD_CONFIGS[build_name],
+                                  **kwargs)
+
+
+class CastComplianceTestRecipe(base_recipe.BaseRecipe):
+  """Recipe to run cast compliance tests against x86 cast shell."""
+
+  def __init__(self, build_name, build_config, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._build_name = build_name
+    self._py3 = build_config.get('py3', False)
+    self._requirements_path = build_config.get(
+        'py_requirements_path', os.path.join('lab_system', 'requirements.txt'))
+
+  def get_steps(self):
+    steps = [
+        cast_shell_step.CastShellStep(BUILD_BRANDING, BUILD_ARCH,
+                                      BUILD_PRODUCT, BUILD_FLAVOR, ENABLE_CHIRP,
+                                      build_args_product=BUILD_ARGS_PRODUCT,
+                                      build_args_official=BUILD_ARGS_OFFICIAL,
+                                      halt_on_failure=True,
+                                      **self._step_kwargs),
+        py_env_setup_step.PyEnvSetupStep(
+            local_packages_dir='team-catatester/python_packages',
+            py3=self._py3,
+            requirements_path=self._requirements_path,
+            requirements_project='test',
+            **self._step_kwargs),
+        x86_integration_test_step.RunIntegrationTestStep(
+            test_case_class=['CastReceiverComplianceTestCase'],
+            # TODO(kiranthampi): re-enable when  b/34853664 is resolved.
+            #                'CastReceiverCapabilityTestCase',
+            #                'CastReceiverMirroringProtocolTestCase',
+            #                'CastReceiverMultipleSendersTestCase',
+            #                'CastReceiverQueueTestCase',
+            #                'CastReceiverStressTestCase',
+            # TODO(mbjorge|kiranthampi): re-enable when b/27219805 is resolved.
+            #                'CastReceiverVolumeControlTestCase'
+            test_case_module='eurtest.tests.cast.receiver.compliance_test_case',
+            test_sequence_class='X86DisabledLoggingTestSequence',
+            test_sequence_module=('eurtest.framework.disabled_logging_'
+                                  'test_sequence'),
+            test_config_file_path='tests.cast.receiver_x86',
+            headless=True,
+            **self._step_kwargs),
+    ]
+    return steps
diff --git a/cq/scripts/slave/recipe/x86_manhattan_test_integration_runner.py b/cq/scripts/slave/recipe/x86_manhattan_test_integration_runner.py
new file mode 100644
index 0000000..44736d6
--- /dev/null
+++ b/cq/scripts/slave/recipe/x86_manhattan_test_integration_runner.py
@@ -0,0 +1,45 @@
+"""Recipe for running integration tests against manhattan shell."""
+from slave import base_recipe
+from slave.step import cast_shell_step
+from slave.step import x86_manhattan_test_integration_step
+
+# TODO(b/24062312): 'clang' --> 'x86'
+BUILD_ARCH = 'clang'
+BUILD_ARGS_PRODUCT = 'displayassistant_x64_eng'
+BUILD_ARGS_OFFICIAL = False
+BUILD_BRANDING = 'chrome'
+BUILD_FLAVOR = 'Debug'
+BUILD_PRODUCT = 'display'
+ENABLE_ASSISTANT = True
+
+
+def GetValidBuildNames():
+  return ['manhattan_shell_crash_loop_test']
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  """Builds a recipe object."""
+  del build_name  # Unused.
+  return ManhattanShellCrashLoopTestRecipe(**kwargs)
+
+
+class ManhattanShellCrashLoopTestRecipe(base_recipe.BaseRecipe):
+  """Recipe to run cast compliance tests against x86 cast shell."""
+
+  def __init__(self, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+
+  def get_steps(self):
+    steps = [
+        cast_shell_step.CastShellStep(BUILD_BRANDING, BUILD_ARCH,
+                                      BUILD_PRODUCT, BUILD_FLAVOR,
+                                      ENABLE_ASSISTANT,
+                                      build_args_product=BUILD_ARGS_PRODUCT,
+                                      build_args_official=BUILD_ARGS_OFFICIAL,
+                                      halt_on_failure=True,
+                                      **self._step_kwargs),
+        x86_manhattan_test_integration_step.RunManhattanShellCrashTestStep(
+            **self._step_kwargs),
+    ]
+    return steps
diff --git a/cq/scripts/slave/recipe/x86_nccm_test_runner.py b/cq/scripts/slave/recipe/x86_nccm_test_runner.py
new file mode 100644
index 0000000..8531e4e
--- /dev/null
+++ b/cq/scripts/slave/recipe/x86_nccm_test_runner.py
@@ -0,0 +1,94 @@
+import os
+
+from slave import base_recipe
+from slave.step import code_coverage_step
+from slave.step import gn_step
+from slave.step import gn_check_step
+from slave.step import x86_camera_step
+
+BUILD_BRANDING = 'chrome'
+BUILD_ARCH = 'x86'
+BUILD_FLAVOR = 'Release'
+BUILD_PRODUCT = 'chromecast'
+BUILD_ARGS_OFFICIAL = False
+BUILD_ARGS_PRODUCT = 'nccm_x86'
+
+COVERAGE_IGNORE_FILENAME_REGEX = '.*/gen/.*|.*/third_party/.*'
+COVERAGE_REPORT_FORMAT = 'lcov'
+
+
+def GetValidBuildNames():
+  return [
+      BUILD_ARGS_PRODUCT,
+  ]
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  del build_name
+  return NccmRecipe(**kwargs)
+
+
+class NccmRecipe(base_recipe.BaseRecipe):
+  """ Recipe for build/run reconnect proxy test"""
+
+  def __init__(self, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._properties = kwargs.get('properties', {})
+    self._code_coverage = self._properties.get('code_coverage', False)
+
+  def get_steps(self):
+    cwd = os.getcwd()
+
+    # Output directory for gn gen step must match expected output directory
+    # for RunCoverageCommandStep based on given BUILD_ARCH, BUILD_PRODUCT, etc.
+    out_dir_parts = ['out', BUILD_ARCH, BUILD_PRODUCT, 'gn']
+    out_dir = os.path.join(
+      'chromium/src', '_'.join(out_dir_parts), BUILD_FLAVOR)
+
+    build_args = []
+    if self._code_coverage:
+      build_args.append('use_clang_coverage=true')
+      build_args.append('is_component_build=false')
+
+    steps = [
+      gn_step.GnGenStep(build_args_product=BUILD_ARGS_PRODUCT,
+                        build_args=build_args,
+                        out_dir=out_dir,
+                        cwd=cwd,
+                        halt_on_failure=True,
+                        **self._step_kwargs),
+      x86_camera_step.BuildNccmUnitTestStep(out_dir=out_dir,
+                                            **self._step_kwargs),
+    ]
+
+    if self._code_coverage:
+      # Code coverage step executes the binary from the step before based
+      # on the list of executables stored in step data and collects
+      # coverage information.
+      coverage_step = code_coverage_step.RunCoverageCommandStep(
+          BUILD_BRANDING,
+          BUILD_ARCH,
+          BUILD_PRODUCT,
+          BUILD_FLAVOR,
+          sanitizer=None,
+          build_args_product=BUILD_ARGS_PRODUCT,
+          build_args_official=BUILD_ARGS_OFFICIAL,
+          ignore_filename_regex=COVERAGE_IGNORE_FILENAME_REGEX,
+          report_format=COVERAGE_REPORT_FORMAT,
+          **self._step_kwargs,
+      )
+      steps.extend([
+          coverage_step,
+          code_coverage_step.CollectLcovCoverageStep(
+              lcov_files=coverage_step.get_lcov_output_file_paths(),
+              **self._step_kwargs,
+          ),
+      ])
+    else:
+      steps += [
+        x86_camera_step.RunNccmUnitTestStep(out_dir=out_dir,
+                                            **self._step_kwargs)
+      ]
+
+    return steps
diff --git a/cq/scripts/slave/recipe/x86_photon_test_runner.py b/cq/scripts/slave/recipe/x86_photon_test_runner.py
new file mode 100644
index 0000000..1f29d60
--- /dev/null
+++ b/cq/scripts/slave/recipe/x86_photon_test_runner.py
@@ -0,0 +1,63 @@
+import os
+
+from slave import base_recipe
+from slave.step import code_coverage_step
+from slave.step import gn_step
+from slave.step import gn_check_step
+from slave.step import x86_camera_step
+
+BUILD_BRANDING = 'chrome'
+BUILD_ARCH = 'x86'
+BUILD_FLAVOR = 'Release'
+BUILD_PRODUCT = 'chromecast'
+BUILD_ARGS_OFFICIAL = False
+BUILD_ARGS_PRODUCT = 'photon_x86'
+
+COVERAGE_IGNORE_FILENAME_REGEX = '.*/gen/.*|.*/third_party/.*'
+COVERAGE_REPORT_FORMAT = 'lcov'
+
+
+def GetValidBuildNames():
+  return [
+      BUILD_ARGS_PRODUCT,
+  ]
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  del build_name
+  return PhotonRecipe(**kwargs)
+
+
+class PhotonRecipe(base_recipe.BaseRecipe):
+  """ Recipe for build/run photon build"""
+
+  def __init__(self, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._properties = kwargs.get('properties', {})
+    self._code_coverage = self._properties.get('code_coverage', False)
+
+  def get_steps(self):
+    cwd = os.getcwd()
+
+    # Output directory for gn gen step must match expected output directory
+    # for RunCoverageCommandStep based on given BUILD_ARCH, BUILD_PRODUCT, etc.
+    out_dir_parts = ['out', BUILD_ARCH, BUILD_PRODUCT, 'gn']
+    out_dir = os.path.join(
+      'chromium/src', '_'.join(out_dir_parts), BUILD_FLAVOR)
+
+    build_args = []
+
+    steps = [
+      gn_step.GnGenStep(build_args_product=BUILD_ARGS_PRODUCT,
+                        build_args=build_args,
+                        out_dir=out_dir,
+                        cwd=cwd,
+                        halt_on_failure=True,
+                        **self._step_kwargs),
+      x86_camera_step.BuildPhotonStep(out_dir=out_dir, **self._step_kwargs),
+      x86_camera_step.RunPhotonUnitTestStep(out_dir=out_dir,
+                                            **self._step_kwargs),
+    ]
+
+    return steps
diff --git a/cq/scripts/slave/recipe/x86_reconnect_proxy_builder.py b/cq/scripts/slave/recipe/x86_reconnect_proxy_builder.py
new file mode 100644
index 0000000..bdf5136
--- /dev/null
+++ b/cq/scripts/slave/recipe/x86_reconnect_proxy_builder.py
@@ -0,0 +1,34 @@
+import os
+
+from slave import base_recipe
+from slave.step import x86_camera_step
+
+
+def GetValidBuildNames():
+  return ['newman_x86_64-eng', 'newman_x64-eng']
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  del build_name
+  return ReconnectProxyRecipe(**kwargs)
+
+
+class ReconnectProxyRecipe(base_recipe.BaseRecipe):
+  """ Recipe for build/run reconnect proxy test"""
+
+  def __init__(self, **kwargs):
+    base_recipe.BaseRecipe.__init__(self, **kwargs)
+    # newman_x86_64 --> newman
+    product = self.buildername[:self.buildername.find('_')]
+    self._step_kwargs.update({
+        'product': product
+    })
+
+  def get_steps(self):
+
+    return [
+      x86_camera_step.BuildReconnectProxyTestStep(
+        **self._step_kwargs),
+      x86_camera_step.RunReconnectTestsStep(
+        **self._step_kwargs),
+    ]
diff --git a/cq/scripts/slave/recipe/x86_simulator_builder.py b/cq/scripts/slave/recipe/x86_simulator_builder.py
new file mode 100644
index 0000000..f413fd8
--- /dev/null
+++ b/cq/scripts/slave/recipe/x86_simulator_builder.py
@@ -0,0 +1,115 @@
+from slave import base_recipe
+from slave.step import code_coverage_step
+from slave.step import x86_camera_step
+
+
+COVERAGE_IGNORE_FILENAME_REGEX = '.*/gen/.*|.*/third_party/.*'
+COVERAGE_REPORT_FORMAT = 'lcov'
+
+
+def GetValidBuildNames():
+  return [
+      'gq_x64-eng',
+      'gq_x86_64-eng',
+      'gq_x64-eng-coverage',
+      'nq_x64-eng',
+      'nq_x86_64-eng',
+      'nq_exp_x86_64-eng',
+      'nq_x64-eng-coverage',
+      'spencer_x64-eng',
+      'spencer_x86_64-eng',
+      'spencer_x64-eng-coverage',
+      'venus_x64-eng',
+      'venus_x86_64-eng',
+      'venus_x64-eng-coverage',
+    ]
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  del build_name
+  return X86SimulatorRecipe(**kwargs)
+
+
+class X86SimulatorRecipe(base_recipe.BaseRecipe):
+  """ Recipe for build x86 64 simulator"""
+
+  def __init__(self, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._code_coverage = self._properties.get('code_coverage', False)
+
+    # nq_x64-eng --> nq
+    product = self.buildername[:self.buildername.find('_')]
+    if ('exp' in self.buildername):
+      product = product + '_exp'
+
+    self._step_kwargs.update({
+      'code_coverage': self._code_coverage,
+      'product': product
+    })
+
+    self.out_dir = f'out_chromecast_{product}_x86_64/release'
+    self.chromium_out_dir = f'chromium/src/{self.out_dir}'
+
+  def get_steps(self):
+    steps = [
+      x86_camera_step.MakeCleanSim(
+        **self._step_kwargs),
+      x86_camera_step.BuildSimulatorStep(
+        **self._step_kwargs),
+      x86_camera_step.BuildChromiumIotTestsStep(
+        out_dir=self.chromium_out_dir, **self._step_kwargs)
+    ]
+
+    if self._code_coverage:
+      # RunCoverageCommandStep already takes a product as an arg.
+      step_kwargs = self._step_kwargs.copy()
+      product = step_kwargs.pop('product') + '_x86_64'
+
+      # Set appropriate LD_LIBRARY_PATH and TARGET_PRODUCT when executing test.
+      # coverage.py executes tests with chromium/src as the working directory
+      # so LD_LIBRARY_PATH has to be relative to chromium/src.
+      test_env = {
+        'LD_LIBRARY_PATH': ':'.join([
+          '../../prebuilt/toolchain/x86_64/usr/lib64',
+          f'../../out/target/product/{product}/system/lib',
+        ]),
+        'TARGET_PRODUCT': product,
+      }
+
+      coverage_step = code_coverage_step.RunCoverageCommandStep(
+          None,
+          None,
+          None,
+          None,
+          sanitizer=None,
+          build_args_product=self.buildername,
+          build_args_official=False,
+          ignore_filename_regex=COVERAGE_IGNORE_FILENAME_REGEX,
+          report_format=COVERAGE_REPORT_FORMAT,
+          build_out_dir=self.out_dir,
+          test_env=test_env,
+          **step_kwargs,
+      )
+      steps.extend([
+          coverage_step,
+          code_coverage_step.CollectLcovCoverageStep(
+              lcov_files=coverage_step.get_lcov_output_file_paths(),
+              **step_kwargs,
+          ),
+      ])
+    else:
+      steps += [
+        x86_camera_step.RunChromiumIotTestsStep(
+          cwd=self.chromium_out_dir,
+          **self._step_kwargs)
+        ]
+
+    steps += [
+      x86_camera_step.CreateSimTarStep(
+        **self._step_kwargs),
+      x86_camera_step.BuildAndRunNldaemonUnitTestStep(
+        **self._step_kwargs),
+    ]
+
+    return steps
diff --git a/cq/scripts/slave/recipe/x86_wifi_simulator_builder.py b/cq/scripts/slave/recipe/x86_wifi_simulator_builder.py
new file mode 100644
index 0000000..8da56f3
--- /dev/null
+++ b/cq/scripts/slave/recipe/x86_wifi_simulator_builder.py
@@ -0,0 +1,93 @@
+from slave import base_recipe
+from slave.step import code_coverage_step
+from slave.step import x86_wifi_step
+from slave.step import code_coverage_step
+
+COVERAGE_IGNORE_FILENAME_REGEX = '.*/gen/.*|.*/third_party/.*'
+COVERAGE_REPORT_FORMAT = 'lcov'
+
+def GetValidBuildNames():
+  return [
+      'sirocco_x86_64-eng',
+      'sirocco_x86_64-eng-coverage',
+    ]
+
+
+def CreateRecipe(build_name: str, **kwargs):
+  del build_name
+  return X86WifiSimulatorRecipe(**kwargs)
+
+
+class X86WifiSimulatorRecipe(base_recipe.BaseRecipe):
+  """ Recipe for build x86 64 simulator"""
+
+  def __init__(self, **kwargs):
+    base_recipe.BaseRecipe.__init__(
+        self, enable_build_accelerator=True, **kwargs)
+    self._code_coverage = self._properties.get('code_coverage', False)
+    self._run_tests = (self._properties.get('run_tests', False)
+      or self._code_coverage)
+    self._step_kwargs.update({
+      'code_coverage': self._code_coverage,
+      'run_tests': self._run_tests,
+    })
+
+    # sirocco_x86_64-eng --> sirocco_x86_64
+    self._product = self.buildername[:self.buildername.find('-')]
+    self.out_dir = f'out_chromecast_{self._product}/release'
+    self.chromium_out_dir = f'chromium/src/{self.out_dir}'
+
+  def get_steps(self):
+
+    steps = [
+      x86_wifi_step.MakeCleanSim(
+        **self._step_kwargs),
+    ]
+
+    if self._run_tests:
+      # For unit test build, we don't need to build the whole simulator,
+      # just unit tests
+      test_env = {
+        'LD_LIBRARY_PATH': ':'.join([
+          self.out_dir,
+          f'../../out/target/product/{self._product}/system/lib',
+        ]),
+        'TARGET_PRODUCT': self._product,
+      }
+      # Always run the unit tests, even in coverage case so we can report
+      # unit test failures in CQ.
+      # TODO(b/261494263): have RunCoverageCommandStep report failure so we
+      # only run tests once per build
+      steps.extend([
+        x86_wifi_step.BuildUnitTestsStep(**self._step_kwargs),
+        x86_wifi_step.RunUnitTestsStep(
+          test_env=test_env,
+          build_out_dir=self.out_dir,
+          **self._step_kwargs),
+      ])
+      if self._code_coverage:
+        coverage_step = code_coverage_step.RunCoverageCommandStep(
+            None, None, None,
+            None, sanitizer=None,
+            build_args_product=self.buildername,
+            build_args_official=False,
+            ignore_filename_regex=COVERAGE_IGNORE_FILENAME_REGEX,
+            report_format=COVERAGE_REPORT_FORMAT,
+            build_out_dir=self.out_dir,
+            test_env=test_env,
+            **self._step_kwargs,
+        )
+        steps.extend([
+            coverage_step,
+            code_coverage_step.CollectLcovCoverageStep(
+                lcov_files=coverage_step.get_lcov_output_file_paths(),
+                **self._step_kwargs,
+            ),
+        ])
+    else:
+      steps.extend([
+        x86_wifi_step.BuildSimulatorStep(**self._step_kwargs),
+        x86_wifi_step.CreateSimTarStep(**self._step_kwargs),
+      ])
+
+    return steps
diff --git a/cq/scripts/slave/step/__init__.py b/cq/scripts/slave/step/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cq/scripts/slave/step/__init__.py
diff --git a/cq/scripts/slave/step/cast_shell_step.py b/cq/scripts/slave/step/cast_shell_step.py
new file mode 100644
index 0000000..1e7511c
--- /dev/null
+++ b/cq/scripts/slave/step/cast_shell_step.py
@@ -0,0 +1,321 @@
+"""Build step class for building x86 cast_shell."""
+import logging
+import os
+import sys
+
+from helpers import branch_utils
+from helpers import cast_shell_utils
+from helpers import gn_utils
+from helpers import ninja_utils
+from slave import base_step
+from slave.step import gn_check_step
+
+_UNIT_TEST_HELPER_MODULE = 'unit_test.unit_test_helper'
+
+BUILD_SCRIPT_PROJECT = 'chromecast/internal'
+BUILD_SCRIPT_REL_PATH = 'build/build_chromecast.py'
+CAST_SHELL_EXE = 'cast_shell'
+
+
+def _InjectPythonPath(path):
+  if path not in sys.path:
+    sys.path.append(path)
+
+
+class NonOtaBuildBaseStep(base_step.BaseStep):
+  """Base step for building and running a non-ota build.
+
+  This is for any non-ota build that uses the build_chromecast.py build flow
+  instead of the OTA `make` build flow.
+  """
+
+  def __init__(self, name, branding, build_type, product, build_flavor,
+      sanitizer=None, build_args_product=None,
+      build_args_official=None, **kwargs):
+    """Creates a UnittestBaseStep instance.
+
+    Args:
+      name: The name of the step.
+      branding: Build branding (chromium or chrome).
+      build_type: Type of system to build for ('x86', 'clang', or 'arm').
+      product: product to build for (ie. 'chromecast', 'audio').
+      build_flavor: Flavor of build ('Debug', 'Eng', 'Release').
+      build_args_official: Whether to do an official build (disables debugging).
+      sanitizer: Clang saniziter to run with ('asan', 'msan', 'tsan', 'ubsan').
+      build_args_product: The build/args/product/*.gn product to build for.
+      build_args_official: Whether the build is an official build. MUST be set
+        for any build that will ship to users. Disables debugging; do not
+        set for development builds.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    super().__init__(name=name, **kwargs)
+
+    # TODO(b/73231972) remove branding, build_type, product, build_flavor
+    # after migration to new builds
+    assert branding in ['chromium', 'chrome', None]
+    assert build_type in ['x86', 'clang', 'arm', 'arm64', None]
+    assert product in [
+        'chromecast', 'audio', 'android', 'atv', 'display', None]
+    assert build_flavor in ['Debug', 'Eng', 'Release', None]
+    assert sanitizer in ['asan', 'msan', 'tsan', 'ubsan', None]
+    assert build_args_product
+    assert build_args_official is not None
+
+    self._branding = branding
+    self._build_type = build_type
+    self._product = product
+    self._build_flavor = build_flavor
+    self._build_args_official = build_args_official
+    self._enable_gn_check = gn_utils.is_gn_check_enabled(self.manifest_branch,
+                                                         product)
+    self._sanitizer = sanitizer
+    self._build_args_product = build_args_product
+
+    self._build_target = kwargs.get('build_target')
+
+    # Re-calculate the out directory. This mimics the calculation done in
+    # build_chromecast.py because we don't want to just import it and we can't
+    # refactor it because we have old branches we need to be able to build.
+    chrome_root = self.get_project_path('chromium/src')
+    self._outdir = os.path.join(chrome_root, 'out_' + build_args_product)
+    if self._build_flavor is None:
+      return
+    build_out_parts = ['out', build_type, product, 'gn']
+    if sanitizer:
+      build_out_parts.append(sanitizer)
+    self._build_out_dir = os.path.join(chrome_root, '_'.join(build_out_parts))
+    flavor_part = 'Release' if build_flavor == 'Eng' else build_flavor
+    self._outdir = os.path.join(self._build_out_dir, flavor_part)
+
+  @property
+  def build_type_dir(self):
+    # TODO(b/73231972) remove build_type based logic once deprecated
+    if self._build_type is None:
+      if '_arm_' in self._build_args_product:
+        return 'arm'
+      elif '_arm64_' in self._build_args_product:
+        return 'arm64'
+      elif '_x86_' in self._build_args_product:
+        return 'x86'
+      return 'clang'
+    return self._build_type
+
+  @property
+  def build_flavor_dir(self):
+    # TODO(b/73231972) remove build_flavor based logic once deprecated
+    # castshell builds had build_flavor == Debug despite non debug suffix
+    if self._build_flavor is None:
+      return 'Debug' if ('debug' in self._build_args_product or
+                         'castshell' in self._build_args_product
+                         ) else 'Release'
+    return 'Release' if self._build_flavor == 'Eng' else self._build_flavor
+
+  @property
+  def build_product_dir(self):
+    # TODO(b/73231972) remove product based logic once deprecated
+    if self._product is None:
+      return 'chromecast'
+    return self._product
+
+  def _get_minimum_required_argv_for_outdir(self):
+    """Returns the arguments needed to get the outdir from build_chromecast.py.
+
+    The chromecast/internal/build/build_chromecast.py script takes many command
+    line arguments, but only a select few are needed to be able to determine
+    the out directory that a build will happen in.
+
+    This helper method assembles the minimum set of arguments required to
+    pass to build_chromecast.py to be generate the correct out directory.
+
+    Returns:
+      A list of arguments
+    """
+    # Set up the argv as if running the build_chromecast.py script.
+    argv = [
+        'build_chromecast.py',
+        '--archive_dir', self.get_gcs_dir(),
+        '--build_args_product', self._build_args_product,
+        '--build_number', self.build_number,
+        '--chrome_root', self.get_project_path('chromium/src')]
+
+    # TODO(b/73231972) remove unused flags once migrated to new builds
+    if self._build_flavor:
+      argv += ['--build_flavor', self._build_flavor]
+    if self._build_type:
+      argv += ['--build_type', self._build_type]
+    if self._branding:
+      argv += ['--chromecast_branding', self._branding]
+    if self._product:
+      argv += ['--product_name', self._product]
+
+    if self._sanitizer:
+      argv += ['--sanitizer', self._sanitizer]
+
+    if self._build_target:
+      argv += ['--build_target', self._build_target]
+
+    return argv
+
+  def run(self):
+    raise NotImplementedError
+
+
+class CastShellStep(NonOtaBuildBaseStep, gn_check_step.GnCheckStepBase):
+  """Build step class for building cast_shell."""
+
+  def __init__(self, branding, build_type, product, build_flavor,
+      enable_assistant, sanitizer=None, archive_to_tmp=False,
+      skip_create_zip=False, strip_binaries=False, **kwargs):
+    """Creates a CastShellStep instance.
+
+    Args:
+      branding: Build branding ('chromium' or 'chrome').
+      build_type: Type of system to build for ('x86', 'clang', or 'arm').
+      product: product to build for (ie. 'chromecast', 'audio').
+      build_flavor: Flavor of build ('Debug', 'Eng', 'Release').
+      enable_assistant: enable Assistant functionality. Audio products only.
+      sanitizer: Clang saniziter to run with ('asan', 'msan', 'tsan', 'ubsan').
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    self._enable_code_coverage = kwargs.pop('code_coverage', False)
+    gn_check_step.GnCheckStepBase.__init__(self, **kwargs)
+    NonOtaBuildBaseStep.__init__(self, 'build cast_shell', branding,
+                                 build_type, product, build_flavor,
+                                 sanitizer=sanitizer, **kwargs)
+    self._enable_assistant = enable_assistant
+    self._archive_to_tmp = archive_to_tmp
+    self._skip_create_zip = skip_create_zip
+    self._strip_binaries = strip_binaries
+    self._unit_test_helper = None
+
+  @property
+  def unit_test_helper(self):
+    """Use the unit_test_helper module to be consistent with Catabuilder."""
+    if not self._unit_test_helper:
+      _InjectPythonPath(self.get_project_path('test'))
+      self._unit_test_helper = __import__(
+          _UNIT_TEST_HELPER_MODULE, fromlist=[''])
+    return self._unit_test_helper
+
+  @property
+  def out_dir(self):
+    return self._outdir
+
+  def BuildCastShell(self):
+    self.archive_dir = \
+      self.get_tmp_dir() if self._archive_to_tmp else self.get_gcs_dir()
+
+    base_path = self.get_project_path(BUILD_SCRIPT_PROJECT)
+    build_script = os.path.join(base_path, BUILD_SCRIPT_REL_PATH)
+    command = ['python3', build_script,
+               '--archive_dir', self.archive_dir,
+               '--build_args_product', self._build_args_product,
+               '--build_number', self.build_number,
+               '--chrome_root', self.get_project_path('chromium/src')]
+    command += self.build_accelerator.build_chromecast_flags
+
+    # TODO(b/73231972) remove unused flags once migrated to new builds
+    if self._build_flavor:
+      command += ['--build_flavor', self._build_flavor]
+    if self._build_type:
+      command += ['--build_type', self._build_type]
+    if self._branding:
+      command += ['--chromecast_branding', self._branding]
+    if self._product:
+      command += ['--product_name', self._product]
+    if self._build_args_official:
+      command += ['--official']
+
+    if self._enable_assistant:
+      command += ['--enable_assistant']
+
+    if self._enable_gn_check:
+      command += ['--enable_gn_check']
+
+    if self._sanitizer:
+      command += ['--sanitizer', self._sanitizer]
+
+    if self._build_target:
+      command += ['--build_target', self._build_target]
+
+    if self._enable_code_coverage:
+      command += ['--enable_code_coverage']
+
+    if self._skip_create_zip:
+      command += ['--skip_create_zip']
+
+    returncode, stdout, _ = self.exec_subprocess(command)
+
+    if returncode != 0:
+      comments = ninja_utils.ninja_failure_to_comments(stdout, max_errors=3)
+      for comment in comments:
+        self.add_review({'message': comment})
+
+      if self._enable_gn_check:
+        self.gn_check_to_review(stdout)
+
+    return returncode
+
+
+  def StripBinaries(self):
+    """Strips binaries and *.so files built."""
+    # out_dir: "chromium/src/out_audioassistant_grte_eng"
+    # build_out_dirname = "out_audioassistant_grte_eng"
+    # buildout_dir_path = "/tmp/Xvsgfvcfg/out_audioassistant_grte_eng"
+    # build_chromecast.py copies files from build_out_dir to buildout_dir_path
+    build_out_dirname = os.path.basename(self._outdir)
+    buildout_dir_path = os.path.join(self.archive_dir, build_out_dirname)
+    binaries_to_strip = []
+
+    if not os.path.exists(buildout_dir_path):
+      print(
+          'Directory %s not found. Skipping strip binaries.'
+          % buildout_dir_path)
+      return
+
+    # Filter out directories and non *.so files from build outputs
+    for path in os.listdir(buildout_dir_path):
+      if os.path.isfile(os.path.join(buildout_dir_path, path)) \
+          and ('.so' in path):
+        binaries_to_strip.append(os.path.join(buildout_dir_path, path))
+
+    CAST_BINARIES_LIST = [
+        'cast_shell',
+        'aurora',
+        'ml_framework',
+        'exo_main',
+        'hello_world'
+    ]
+
+    # Add cast binaries to binaries_to_strip
+    for path in os.listdir(buildout_dir_path):
+      if os.path.isfile(os.path.join(buildout_dir_path, path)) \
+          and (path in CAST_BINARIES_LIST):
+        binaries_to_strip.append(os.path.join(buildout_dir_path, path))
+
+    # Strip binaries to same path's
+    for binary_path in binaries_to_strip:
+      print('Stripping %s' % binary_path)
+      try:
+        self.unit_test_helper.StripBinary(binary_path)
+      except Exception as e:
+        logging.error(
+            'Error while stripping binary %s %s' % (binary_path, str(e)))
+
+  def run(self):
+    """Builds cast_shell.
+
+    Returns:
+      True iff there were no errors.
+    """
+    returncode = self.BuildCastShell()
+
+    if self._strip_binaries:
+      self.StripBinaries()
+
+    cast_shell_exe_path = os.path.join(self._outdir, CAST_SHELL_EXE)
+    self.add_step_data(cast_shell_utils.CAST_SHELL_EXE_PATH,
+                       cast_shell_exe_path)
+    print("Cast shell exe path: ", cast_shell_exe_path)
+
+    return returncode == 0
diff --git a/cq/scripts/slave/step/catatester_binaries_step.py b/cq/scripts/slave/step/catatester_binaries_step.py
new file mode 100644
index 0000000..849df1f
--- /dev/null
+++ b/cq/scripts/slave/step/catatester_binaries_step.py
@@ -0,0 +1,115 @@
+"""Build step class for building and testing test binaries."""
+
+from __future__ import absolute_import
+import logging
+import os
+import tempfile
+
+from helpers import cipd_upload_utils
+from helpers import py_env_utils
+from slave import base_step
+
+
+class CatatesterBinariesException(Exception):
+  """Raised on step exceptions."""
+
+
+class CatatesterBinariesBuildStep(base_step.BaseStep):
+  """Step for building Catatester Binaries."""
+
+  def __init__(self, py3: bool, **kwargs):
+    """Creates a CatatesterBinariesBuildStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self,
+                                name='build catatester binaries',
+                                **kwargs)
+    self.py3 = py3
+
+  def _get_configs_path(self):  # pylint: disable=method-hidden
+    """Path to the configs file."""
+    continuous_tests = os.path.abspath(
+        self.get_project_path('continuous-tests'))
+    return os.path.join(continuous_tests, 'lab_system', 'master', 'config')
+
+  def _get_python_path(self):
+    """Get the PYTHONPATH to execute tests with.
+
+    This method was written based on test/setup_env.sh
+    """
+    python_path = []
+
+    python_path.append(os.path.abspath(self.get_project_path('test')))
+
+    # continuous-tests/ has to be above all the other repositories
+    continuous_tests_repo = os.path.abspath(
+        self.get_project_path('continuous-tests'))
+    python_path.append(continuous_tests_repo)
+
+    chromium_repo = os.path.abspath(self.get_project_path('chromium/src'))
+    chromium_telemetry_repo = os.path.join(
+        chromium_repo, 'third_party', 'catapult', 'telemetry')
+    python_path.append(chromium_telemetry_repo)
+    python_path.append(os.path.join(
+        chromium_telemetry_repo, 'third_party', 'websocket-client'))
+    python_path.append(os.path.join(chromium_repo, 'chromecast', 'tools'))
+
+    python_path.append(os.path.join(continuous_tests_repo, 'cq'))
+
+    python_path.append(os.path.abspath(
+        self.get_project_path('chromiumos/third_party/autotest')))
+    python_path.append(os.path.abspath(
+        self.get_project_path('chromiumos/chromite')))
+
+    sprockets_repo = os.path.abspath(self.get_project_path('google/sprockets'))
+    python_path.append(sprockets_repo)
+
+    return os.pathsep.join(python_path)
+
+  def _get_env_path(self, venv_root):
+    """Prepends virtualenv bin directory to existing $PATH var."""
+    venv_bin = os.path.join(venv_root, 'bin')
+    current_path_var = os.getenv('PATH')
+    return os.pathsep.join([venv_bin, current_path_var])
+
+  def _build_binaries(self, venv_root):
+    """Generates pyinstaller spec files and builds binaries.
+
+    Returns True if build succeeds, else False.
+    """
+    venv_root = self.get_step_data('virtual_env_root')
+    binary_generator = os.path.join(
+        self.get_project_path('test'),
+        'swarming', 'pyinstaller_tools', 'binary_generator.py')
+    cmd = [binary_generator]
+
+    env = {}
+    env['FRAGGLE_ROCK_CONFIG_FILES'] = self._get_configs_path()
+    env['PYTHONPATH'] = self._get_python_path()
+    env['PATH'] = self._get_env_path(venv_root)
+
+    returncode, stdout, stderr = py_env_utils.exec_within_virtual_env(
+        self, venv_root, cmd, env=env)
+
+    if returncode != 0:
+      logging.error('Failed to generate binaries: %s\n%s', stdout, stderr)
+      return False
+
+    return True
+
+  def run(self):
+    """Creates all catatester binaries.
+
+    Sets a build property "review" with the output in case of failure.
+
+    Returns:
+      True if there were no test errors, else False.
+    """
+    venv_root = self.get_step_data('virtual_env_root')
+    success = self._build_binaries(venv_root)
+    if not success:
+      # TODO(adrexler): Provide more useful failure message
+      self.add_review({'message': 'Failed to generate all binaries.'})
+    return success
diff --git a/cq/scripts/slave/step/ccstudio_step.py b/cq/scripts/slave/step/ccstudio_step.py
new file mode 100644
index 0000000..223d96e
--- /dev/null
+++ b/cq/scripts/slave/step/ccstudio_step.py
@@ -0,0 +1,60 @@
+"""Build step class for CCStudio build."""
+
+from __future__ import absolute_import
+import os
+
+from slave import base_step
+
+class CCStudioStep(base_step.BaseStep):
+  """Build step class for CCStudio build."""
+
+  def __init__(self, build_name, name='ccstudio build', **kwargs):
+    """Create a CCStudioStep instance.
+
+    Args:
+      build_name: Name of build (e.g. abbey-eng).
+      name: User-visible name of this step.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name=name, **kwargs)
+    self._build_name = build_name
+
+  def _build(self):
+    """Build |build_name| image by CCStudioStep.
+
+    Returns:
+      True if image was built successfully; False if failed.
+    """
+    venv_root = self.get_step_data('virtual_env_root', '/usr')
+    python_path = os.path.join(venv_root, 'bin')
+    ccs_dir = os.path.expanduser('~/ccstudio/ccs/eclipse')
+    src_dir = self.get_project_path('abbey')
+    dist_dir = self.get_gcs_dir()
+    command = [
+        os.path.join(src_dir, 'build', 'build.sh'),
+        '--python_path={python_path}'.format(python_path=python_path),
+        '--ccs_dir={ccs_dir}'.format(ccs_dir=ccs_dir),
+        '--src_dir={src_dir}'.format(src_dir=src_dir),
+        '--dist_dir={dist_dir}'.format(dist_dir=dist_dir),
+        '--build_name={build_name}'.format(build_name=self._build_name),
+    ]
+    returncode, stdout, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      self._error_processor(stdout, stderr)
+      return False
+    return True
+
+  def run(self):
+    """Build images.
+
+    Returns:
+      True if there were no errors.
+    """
+    return self._build()
+
+  def _error_processor(self, stdout, stderr):
+    # TODO(b/141637066): Improve error handling
+    if stdout:
+      self.add_review({'message': stdout})
+    if stderr:
+      self.add_review({'message': stderr})
diff --git a/cq/scripts/slave/step/cipd_upload_step.py b/cq/scripts/slave/step/cipd_upload_step.py
new file mode 100644
index 0000000..ce4a1b8
--- /dev/null
+++ b/cq/scripts/slave/step/cipd_upload_step.py
@@ -0,0 +1,91 @@
+"""Step class for uploading new CIPD packages."""
+from __future__ import absolute_import
+import logging
+import os
+
+from helpers import cipd_upload_utils
+from slave import base_step
+
+
+class CIPDUploadError(Exception):
+  """Raised when an error occurs with CIPDUploadStep."""
+
+
+class CIPDUploadStep(base_step.BaseStep):
+  """Uploads a new CIPD package."""
+
+  def __init__(self, pkg_def, pkg_vars=None, refs=None, tags=None,
+               verification_timeout=None, step_name='Upload CIPD Package',
+               **kwargs):
+    """Constructor.
+
+    Args:
+      pkg_def: The path to the pkg_def file relative to the build root.
+      pkg_vars: A dict of variables to pass to cipd with -pkg-var.
+      refs: A list of refs to be assigned to the uploaded cipd package.
+      tags: A dict of tags to be assigned to the uploaded cipd package.
+      verification_timeout: Time allotted for cipd verification step ('XXmXXs').
+      step_name: name of the Buildbot step. Used for uniquely identifying the
+                 step and for display in the builder status.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self,
+                                name=step_name,
+                                **kwargs)
+    self._cipd_upload = self.build_system == 'catabuilder'
+    if not self._cipd_upload:
+      self._name += ' DRY RUN'
+    self._pkg_def_path = pkg_def
+    self._pkg_vars = pkg_vars
+    self._refs = refs
+    self._tags = self._GetDefaultTags()
+    self._tags.update(tags or {})
+    self._verification_timeout = verification_timeout
+
+  def _GetDefaultTags(self):
+    """Returns a dict of default tags to be included on all CIPD uploads."""
+    tags = {}
+
+    tags['system'] = '_'.join([self.build_system, self.env])
+
+    if 'buildset' in self._properties:
+      tags['buildset'] = self.get_property('buildset')
+
+    return tags
+
+  # TODO(b/144364136) remove support JSON support after migration to GCE
+  def _GetServiceAccountJson(self):
+    """Returns path to service_account_json param for current environment."""
+    path = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS')
+    if not path and self._cipd_upload:
+      path = ':gce'
+    return path
+
+  def run(self):
+    """Uploads the designated CIPD package.
+
+    Returns:
+      True iff CIPD package is uploaded successfully.
+    """
+    service_account_json = self._GetServiceAccountJson()
+
+    returncode, _, stderr = cipd_upload_utils.UploadCIPDPackage(
+        self,
+        self._pkg_def_path,
+        pkg_vars=self._pkg_vars,
+        refs=self._refs,
+        tags=self._tags,
+        verification_timeout=self._verification_timeout,
+        service_account_json=service_account_json,
+        dry_run=not self._cipd_upload)
+
+    if returncode == 0:
+      if self._cipd_upload:
+        logging.info('Package uploaded successfully: %s', self._pkg_def_path)
+      return True
+
+    message = 'CIPD upload for {} exited with nonzero returncode {}. {}'.format(
+        self._pkg_def_path, returncode, stderr)
+    logging.error(message)
+    self.add_review({'message': message})
+    return False
diff --git a/cq/scripts/slave/step/clang_format_step.py b/cq/scripts/slave/step/clang_format_step.py
new file mode 100644
index 0000000..e7a4074
--- /dev/null
+++ b/cq/scripts/slave/step/clang_format_step.py
@@ -0,0 +1,129 @@
+"""Build step class for formatting C++ files with clang-format-diff."""
+
+from __future__ import absolute_import
+import logging
+import os
+
+from helpers import commit_message_utils
+from helpers import diff_utils
+from helpers import git_utils
+from slave import base_step
+
+CLANG_FORMAT_DIFF_SCRIPT = 'scripts/helpers/clang_format/clang-format-diff.py'
+GIT_DIFF_COMMAND = ['git', 'diff', '-U0', 'HEAD^']
+CLANG_FORMAT_ERROR_MSG = ('Fix format errors with `git cast format` '
+                          '(see go/git-cast for more info)')
+
+
+class ClangFormatStep(base_step.BaseStep):
+  """Build step class for clang-formatting C++ files."""
+
+  def __init__(self, **kwargs):
+    """Creates a ClangFormatStep instance.
+
+    Note that this step will only format files with the following extensions:
+      .cc, .cpp, .cxx, .c++, .c, .cl, .h, .hpp, .m, .mm, .inc, .js, .proto,
+      .protodevel, .java
+
+    Note that this step uses clang-format-diff, so that only the changed
+    parts of a file are re-formatted.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='clang-format', **kwargs)
+
+  def _filter_comments(self, comments):
+    for filename in list(comments):
+      if self.allow_listed(filename) and not self.deny_listed(filename):
+        continue
+      del comments[filename]
+    return comments
+
+  def _clang_format_binary(self):
+    """Returns the path to the clang-format binary.
+
+    This first attempts to find the clang-format from buildtools, but will
+    fall back to the latest pre-installed clang format if it cannot find it.
+
+    Returns:
+      Path to the clang-format binary.
+    """
+    try:
+      chromium_src = self.get_project_path('chromium/src')
+      clang_format_binary = os.path.join(chromium_src, 'buildtools', 'linux64',
+                                         'clang-format')
+      if os.path.exists(clang_format_binary):
+        return clang_format_binary
+    except KeyError:
+      pass
+
+    return 'clang-format-3.6'
+
+  def _clang_format_diff_script(self):
+    """Returns the path of the clang-format-diff.py script."""
+    try:
+      chromium_src = self.get_project_path('chromium/src')
+      clang_format_diff_script = os.path.join(chromium_src, 'buildtools',
+                                              'clang_format', 'script',
+                                              'clang-format-diff.py')
+      if os.path.exists(clang_format_diff_script):
+        return clang_format_diff_script
+    except KeyError:
+      pass
+
+    return os.path.join(self.get_cq_root(), CLANG_FORMAT_DIFF_SCRIPT)
+
+  def run(self):
+    """Runs clang-format-diff on changed files for the current commit.
+
+    Sets a build property "review" with the output.
+
+    Returns:
+      True iff there were no formatting errors.
+    """
+    # Do not format cherry-picks; errors are ignored for upstream consistency
+    if git_utils.is_cherry_pick(self, self.directory):
+      logging.info('clang_format check skipped because this is a cherry-pick.')
+      return True
+
+    diff_command = (GIT_DIFF_COMMAND, {'cwd': self.directory})
+    clang_format_diff = os.path.relpath(self._clang_format_diff_script(),
+                                        self.directory)
+    clang_format = os.path.relpath(self._clang_format_binary(), self.directory)
+    format_command = (['python3',
+                       clang_format_diff,
+                       '-binary',
+                       clang_format,
+                       '-p1',
+                       '-style',
+                       'file',], {'cwd': self.directory})
+
+    returncode, stdout, _ = self.exec_subprocess_chain([diff_command,
+                                                        format_command])
+
+    if returncode == 0 and not stdout:
+      logging.info('clang_format check received no output on stdout')
+      return True
+
+    comments = diff_utils.diff_to_comments(stdout.splitlines())
+    comments = self._filter_comments(comments)
+
+    if not comments:
+      # All the comments were filtered out, so this was actually a success.
+      logging.info('clang_format had no comments after filtering.')
+      return True
+
+    self.add_review({'message': CLANG_FORMAT_ERROR_MSG, 'comments': comments})
+    # Do not block the build if this is a revert.
+    commit_message = git_utils.commit_message(self, self.directory)
+    if commit_message_utils.is_revert_commit_message(commit_message):
+      logging.info('looks like a revert, allowing it through.')
+      return True
+    logging.info('clang_format failure, adding %d comments.', len(comments))
+    logging.info('If you believe that your project should be exempt from '
+      'formatting requirements you can follow the instructions at '
+      'go/cast-format-exemption in order to disable formatting checks '
+      'on your code. This is often useful for projects that pull code '
+      'from third-party sources.')
+    return False
diff --git a/cq/scripts/slave/step/clang_tidy_step.py b/cq/scripts/slave/step/clang_tidy_step.py
new file mode 100644
index 0000000..6a90d1e
--- /dev/null
+++ b/cq/scripts/slave/step/clang_tidy_step.py
@@ -0,0 +1,239 @@
+"""Build step class for formatting C++ files with clang-tidy-diff."""
+
+from __future__ import absolute_import
+import logging
+import os
+from collections import defaultdict
+import yaml
+
+from helpers import commit_message_utils
+from helpers import diff_utils
+from helpers import git_utils
+from slave import base_step
+
+CLANG_FORMAT_DIFF_SCRIPT = 'clang-tidy-diff.py'
+GIT_DIFF_COMMAND = ['git', 'diff', '-U0', 'HEAD^']
+
+# TODO(mingcl): point the go link to g3doc and add instructions/explanations
+# there
+CLANG_TIDY_MSG = (
+    'ClangTidy have some suggestions. This will not block submission. See '
+    'go/eureka-clang-tidy')
+
+
+# Given file name and character offset, return a tuple consisting line number
+# and character position in the line
+def char_offset_to_line_number(filename, char_offset):
+  try:
+    with open(filename) as f:
+      content = f.read(char_offset)
+      line_number = content.count('\n') + 1
+      line_start_position = content.rfind('\n')
+      if line_start_position == -1:
+        char_position = char_offset
+      else:
+        char_position = char_offset - (line_start_position + 1)
+      return (line_number, char_position)
+  except (OSError, IOError) as _:
+    logging.info('Cannot open {} in repo. '.format(filename) +
+                 'It might be a generated file. ' +
+                 'Returning arbitrary line number and offset ' +
+                 'because Gerrit will ignore this comment anyway.')
+    return (1, 0)
+
+
+# Given file, offset and length, convert it to CommentRange object that Gerrit
+# API accepts
+def to_comment_range_object(base_dir, file_path, offset, length):
+  (start_line, start_character) = char_offset_to_line_number(
+      os.path.join(base_dir, file_path), offset)
+  (end_line, end_character) = char_offset_to_line_number(
+      os.path.join(base_dir, file_path), offset + length)
+  return {
+      'start_line': int(start_line),
+      'start_character': int(start_character),
+      'end_line': int(end_line),
+      'end_character': int(end_character)
+  }
+
+
+# Given git repo root and file_path from the repo, normalize and convert
+# file_path to the relative path from repo_root.
+# This is needed because pathes outputed by clang-tidy-diff script might
+# contain redundant double dots
+def path_relative_to_repo_root(repo_root, file_path):
+  return os.path.relpath(
+      os.path.normpath(os.path.join(repo_root, file_path)), repo_root)
+
+
+def suggestion_to_gerrit_comments(suggestions_str, base_dir):
+  r""" Convert a YAML clang_tidy output to gerrit comment format
+
+  Comments are formatted according to:
+    https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-input
+
+  Args:
+    suggestions_str: YAML string exported by clang tidy
+    base_dir: Absolution path of the directory which the clang tidy command is
+      run in
+  """
+  if not suggestions_str:
+    return {}
+
+  diagnostics = yaml.safe_load(suggestions_str).get('Diagnostics', [])
+
+  if not diagnostics:
+    return {}
+
+  comments = defaultdict(list)
+
+  for diagnostic in diagnostics:
+    diagnostic_name = diagnostic['DiagnosticName']
+    diagnostic_message = diagnostic['DiagnosticMessage']
+    diagnostic_level = diagnostic['Level']
+    # Don't report errors. They will be caught by other builders anyway.
+    if diagnostic_level == 'Error':
+      continue
+    comments[path_relative_to_repo_root(
+        base_dir, diagnostic_message['FilePath'])].append({
+            'message':
+                '[non-blocking] ClangTidy: [{}] {}: {}'.format(
+                    diagnostic_name, diagnostic_level,
+                    diagnostic_message['Message']),
+            'range':
+                to_comment_range_object(base_dir,
+                                        diagnostic_message['FilePath'],
+                                        diagnostic_message['FileOffset'], 1)
+        })
+    for replacement in diagnostic_message['Replacements']:
+      if replacement['Length'] > 0 and replacement['ReplacementText']:
+        replacement_suggestion_message = 'Replace this with `' + replacement[
+            'ReplacementText'] + '`.'
+      elif replacement['Length'] > 0:
+        replacement_suggestion_message = 'Remove this.'
+      elif replacement['ReplacementText']:
+        replacement_suggestion_message = 'Insert `' + replacement[
+            'ReplacementText'] + '` here.'
+        # Avoid zero length comment length to accurately indicate the location
+        # of insertion
+        replacement['Length'] = 1
+      else:
+        # It makes no sense to suggest to replace empty string with empty
+        # string. Leave the suggestion message empty
+        replacement_suggestion_message = ''
+      comments[path_relative_to_repo_root(
+          base_dir, replacement['FilePath'])].append({
+              'message':
+                  '[non-blocking] ClangTidy: [{}] {}\n{}'.format(
+                      diagnostic_name, diagnostic_message['Message'],
+                      replacement_suggestion_message),
+              'range':
+                  to_comment_range_object(base_dir, replacement['FilePath'],
+                                          replacement['Offset'],
+                                          replacement['Length'])
+          })
+    # If Notes exists, also convert them to comments
+    for note in diagnostic.get('Notes', []):
+      comments[path_relative_to_repo_root(base_dir, note['FilePath'])].append({
+          'message':
+              '[non-blocking] ClangTidy: [{}] {}'.format(
+                  diagnostic_name, note['Message']),
+          'range':
+              to_comment_range_object(base_dir, note['FilePath'],
+                                      note['FileOffset'], 1)
+      })
+  return comments
+
+
+class ClangTidyStep(base_step.BaseStep):
+  """Build step class for clang-tidying C++ files."""
+
+  def __init__(self, **kwargs):
+    """Creates a ClangTidyStep instance.
+
+    Note that this step uses clang-tidy-diff, so that only the changed
+    parts of a file are checked.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='clang-tidy', **kwargs)
+
+  def _filter_comments(self, comments):
+    for filename in list(comments):
+      if self.allow_listed(filename) and not self.deny_listed(filename):
+        continue
+      del comments[filename]
+    return comments
+
+  def run(self):
+    """Runs clang-tidy-diff on changed files for the current commit.
+
+    Sets a build property "review" with the output.
+
+    Returns:
+      True iff there were no suggestions.
+    """
+    # Do not run on cherry-picks; errors are ignored for upstream consistency
+    if git_utils.is_cherry_pick(self, self.directory):
+      logging.info('clang_tidy check skipped because this is a cherry-pick.')
+      return True
+
+    # Do not run if the build looks like a revert.
+    commit_message = git_utils.commit_message(self, self.directory)
+    if commit_message_utils.is_revert_commit_message(commit_message):
+      logging.info('looks like a revert, allowing it through.')
+      return True
+
+    # Ensure there is prebuilt clang-tidy binary available in
+    # chromium/src/third_party/llvm-build/Release+Asserts/bin/
+    # It won't be there if this step has never been run before.
+    returncode, _, _ = self.exec_subprocess([
+        'python3',
+        os.path.join(
+            self.get_project_path('chromium/src'),
+            'tools/clang/scripts/update.py'), '--package', 'clang-tidy'
+    ])
+    if returncode != 0:
+      return False
+
+    output_file = 'clang_tidy_fixes.yaml'
+
+    diff_command = (GIT_DIFF_COMMAND, {'cwd': self.directory})
+    clang_tidy_command = ([
+        'clang-tidy-diff.py',
+        '-clang-tidy-binary',
+        os.path.join(self._getcwd(), self.get_project_path('chromium/src'),
+                     'third_party/llvm-build/Release+Asserts/bin/clang-tidy'),
+        '-p1',
+        '-export-fixes',
+        output_file,
+    ], {
+        'cwd': self.directory
+    })
+
+    returncode, stdout, _ = self.exec_subprocess_chain(
+        [diff_command, clang_tidy_command])
+    if returncode != 0:
+      return False
+
+    clang_tidy_fixes = ''
+    try:
+      with open(os.path.join(self.directory, output_file)) as file:
+        clang_tidy_fixes = file.read()
+      logging.info('yaml outputed by clang-tidy:\n%s', clang_tidy_fixes)
+    except (OSError, IOError) as _:
+      logging.info('clang tidy suggestion file not exist')
+
+    gerrit_comments = suggestion_to_gerrit_comments(clang_tidy_fixes,
+                                                    self.directory)
+
+    gerrit_comments = self._filter_comments(gerrit_comments)
+
+    if gerrit_comments == {}:
+      logging.info('clang_tidy has no comments')
+      return True
+
+    self.add_review({'message': CLANG_TIDY_MSG, 'comments': gerrit_comments})
+
+    return False
diff --git a/cq/scripts/slave/step/clean_sync_step.py b/cq/scripts/slave/step/clean_sync_step.py
new file mode 100644
index 0000000..ac7b513
--- /dev/null
+++ b/cq/scripts/slave/step/clean_sync_step.py
@@ -0,0 +1,164 @@
+"""Build step class for setting up a clean build environment."""
+
+import logging
+import os
+import re
+
+from helpers import gclient_utils
+from slave import base_step
+from slave.step import shell_step
+
+_GCLIENT_CWD = 'chromium'
+_GCLIENT_ENV = {
+    # Set libassistant retries to wait for binaries to be compiled
+    # Expected wait time is about 900 sec.
+    'LIBASSISTANT_FINDARCHIVE_RETRY_COUNT':
+        '15',
+    'LIBASSISTANT_FINDARCHIVE_RETRY_INTERVAL':
+        '300',
+    # Export CHROME_HEADLESS so that the license agreement is
+    # automatically accepted in //build/android/play_services/update.py.
+    'CHROME_HEADLESS':
+        '1',
+
+    # Set this so `gclient sync` does not update the ninja files with GYP.
+    # This call is very expensive and is unecessary: On GN builds, GN is
+    # used to generate the ninja files, and on GYP builds, we do this as a
+    # later step.
+    'GYP_CHROMIUM_NO_ACTION':
+        '1',
+
+    # Set this so `gclient sync` will pick up the prebuilt,
+    # instrumented libraries that msan needs to run effectively.
+    # Specifically, gclient sync will run
+    # //third_party/intrumented_libraries/scripts/download_binaries.py$
+    # to download the prebuilt library binaries.
+    'GYP_DEFINES': ('use_prebuilt_instrumented_libraries=1 '
+                    'msan=1 '
+                    'msan_track_origins=2'),
+}
+
+_FUCHSIA_SYNC_ENV_VAR = 'FUCHSIA_SYNC'
+
+_INSERT_SHA = '{INSERT_SHA}'
+_INSERT_PROJECT = '{INSERT_PROJECT}'
+
+_GCLIENT_ERROR_RECOVERY = {
+    'Cannot fast-forward merge, attempt to rebase?': [
+        'git', 'reset', '--hard', _INSERT_SHA
+    ],
+    'Conflict while rebasing this branch.': ['git', 'am', '--abort'],
+    'Already in a conflict, i.e. (no branch).': ['git', 'am', '--abort'],
+    'Command \'git rev-list -n 1 HEAD\' returned non-zero exit status 128': [
+        'git', 'pull'
+    ],
+    'subprocess.CalledProcessError': [
+        'git', 'submodule', 'deinit', '--all', '-f'
+    ],
+    'fetch_post_sync_builddeps.SubmoduleSyncError': [
+        'rm', '-rf', _INSERT_PROJECT
+    ],
+}
+
+UNIX_SUCCESS_CODE = 0
+
+
+class UnhandledGClientSyncError(base_step.StepError):
+  """Exception raised when an unrecognized gclient sync failure happens."""
+
+
+class GclientSyncStep(shell_step.ShellStep):
+  """Runs `gclient sync` after patches have been applied."""
+
+  def __init__(self, **kwargs):
+    """Creates a GclientSyncStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    shell_step.ShellStep.__init__(
+        self, name='gclient sync', halt_on_failure=True, **kwargs)
+    self._gclient_root = kwargs.get('properties',
+                                    {}).get('gclient_root', _GCLIENT_CWD)
+    self._fuchsia_sync = kwargs.get('properties', {}).get('fuchsia_sync', False)
+    self._gclient_extra_args = kwargs.get('properties',
+                                          {}).get('gclient_extra_args')
+
+  def _setup_for_repo_sync_retry(self, returncode, stdout, stderr):
+    """A function to run before retrying a failed gclient sync."""
+    del returncode  # Unused.
+
+    for recoverable in _GCLIENT_ERROR_RECOVERY:
+      if recoverable in stderr:
+        reg_exp = r'^(?:\d+> )?_+ (?P<project_dir>[^\s]+) at (?P<sha>.*)$'
+        match = re.search(reg_exp, stderr, re.MULTILINE)
+        # See b/112112992 for context on the error.
+        if match:
+          project_dir = match.group('project_dir')
+          project_dir = os.path.join(self._gclient_root, project_dir)
+          sha = match.group('sha')
+          command = self._insert_template(_GCLIENT_ERROR_RECOVERY[recoverable],
+                                          _INSERT_SHA, sha)
+          self.exec_subprocess(command, cwd=project_dir)
+          break
+
+        reg_exp = (r'Command \'git rev-list -n 1 HEAD\' '
+                   r'returned non-zero exit status 128 in (?P<project_dir>.*)')
+        match = re.search(reg_exp, stderr, re.MULTILINE)
+        if match:
+          project_dir = match.group('project_dir')
+          self.exec_subprocess(
+              _GCLIENT_ERROR_RECOVERY[recoverable], cwd=project_dir)
+          break
+        logging.info('Found error in stderr but fail to recover: %s',
+                     recoverable)
+      elif recoverable in stdout:
+        reg_exp = (r'subprocess\.CalledProcessError: Command '
+                   r'\'\[\'git\', \'-C\', \'(?P<project_dir>.+)\', '
+                   r'\'submodule\', \'sync\'\]\' returned non-zero exit status')
+        match = re.search(reg_exp, stdout, re.MULTILINE)
+        if match:
+          project_dir = match.group('project_dir')
+          self.exec_subprocess(
+              _GCLIENT_ERROR_RECOVERY[recoverable], cwd=project_dir)
+          break
+
+        # See b/135531243 for context on the error.
+        reg_exp = r'(?P<project_name>.+) submodules failed to sync'
+        match = re.search(reg_exp, stdout, re.MULTILINE)
+        if match:
+          project_dir = self.get_project_path(match.group('project_name'))
+          command = self._insert_template(_GCLIENT_ERROR_RECOVERY[recoverable],
+                                          _INSERT_PROJECT, project_dir)
+          self.exec_subprocess(command)
+          break
+        logging.info('Found error in stdout but fail to recover: %s',
+                     recoverable)
+
+  def _insert_template(self, command, template, value):
+    return [w if w != template else value for w in command]
+
+  def get_commands(self):
+    # Ensures that additions to the gclient env here will remain deterministic.
+    env = dict(_GCLIENT_ENV)
+    if self._fuchsia_sync:
+      env[_FUCHSIA_SYNC_ENV_VAR] = '1'
+    return [
+        # Bazel uses a shared location for extracting its installation.
+        # Sometimes those installations get corrupt. This forces Bazel to
+        # extract itself for every build.
+        self.ShellCommand(['rm', '-rf',
+                           os.path.expanduser('~/.cache/bazel')]),
+        self.ShellCommand(gclient_utils.disable_depot_tools_auto_update_cmd()),
+        self.ShellCommand(  # pylint: disable=unexpected-keyword-arg
+            gclient_utils.gclient_sync_cmd(
+                self._executor,
+                jobs=self.get_num_jobs(),
+                extra_args=self._gclient_extra_args,
+            ),
+            cwd=self._gclient_root,
+            env=env,
+            num_retries=3,
+            setup_for_retry=self._setup_for_repo_sync_retry,
+            halt_on_failure=True),
+    ]
diff --git a/cq/scripts/slave/step/code_coverage_step.py b/cq/scripts/slave/step/code_coverage_step.py
new file mode 100644
index 0000000..12f16e4
--- /dev/null
+++ b/cq/scripts/slave/step/code_coverage_step.py
@@ -0,0 +1,222 @@
+"""Build step class for generating code coverage reports."""
+import fileinput
+import logging
+import os
+import pathlib
+import shutil
+import time
+from slave import base_step
+from slave.step import cast_shell_step
+from cq.scripts.helpers import coverage_utils
+
+COVERAGE_SCRIPT_PATH = 'tools/code_coverage/coverage.py'
+COVERAGE_REPORT_PATH = 'code_coverage_html'
+COVERAGE_LCOV_PATH = f'{COVERAGE_REPORT_PATH}/linux/coverage.lcov'
+STEP_DATA_TEST_LIST = 'step_data_test_list'
+
+# This is a list of test suites to filter out of the coverage
+# run.  Tests are added to this list of they crash, or otherwise disrupt the
+# generation of code coverage data.
+TESTS_TO_FILTER_OUT = [
+    'bindings_browsertests',  # b/178190494
+    'cast_shell_browsertests',  # b/178190494
+    'cast_shell_internal_browsertests',  # b/178190494
+]
+
+# TODO(b/232528731): Can potentially try to find where this is defined
+#                    dynamically to avoid hardcoding it. If possible,
+#                    use a configuration variable instead.
+PREFIX_FILTER = '/b/f/w'
+
+
+class RunCoverageCommandStep(cast_shell_step.NonOtaBuildBaseStep):
+  """Build step class generating code coverage data."""
+
+  def __init__(self, branding, build_type, product, build_flavor,
+               sanitizer=None, build_args_product=None, junit=False,
+               ignore_filename_regex=None, report_format=None,
+               build_out_dir=None, test_env=None, **kwargs):
+    """Creates a RunCoverageCommandStep instance.
+
+    Args:
+      branding: Build branding (chromium or chrome).
+      build_type: Type of system to build for ('x86', 'clang', or 'arm').
+      product: product to build for (ie. 'chromecast', 'audio').
+      build_flavor: Flavor of build ('Debug', 'Eng', 'Release').
+      sanitizer: Clang sanitizer to run with ('asan', 'msan', 'tsan', 'ubsan').
+      build_args_product: The name of one of the
+        chromecast/internal/build/args/product files to use for the GN args.
+      junit: If true, run junit tests, otherwise run gtests (Default False).
+      ignore_filename_regex: Skip source code files with file paths that match
+        the given regular expression.
+      report_format: Output format of the "llvm-cov show/export" command
+        ('html', 'text', 'lcov').
+      build_out_dir: Build output directory.
+      test_env: Dictionary of env variables to set while executing tests.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    cast_shell_step.NonOtaBuildBaseStep.__init__(
+        self, 'generate coverage data', branding, build_type,
+        product, build_flavor, sanitizer=sanitizer, timeout_secs=0,
+        build_args_product=build_args_product, **kwargs)
+
+    self._junit = junit
+    self._ignore_filename_regex = ignore_filename_regex
+    self._format = report_format
+    self._build_out_dir = build_out_dir
+    self._test_env = test_env
+
+  def _get_build_out_dir(self, use_flavor=False):
+    if self._build_out_dir:
+      return self._build_out_dir
+
+    out_dir = ['out', self.build_type_dir, self.build_product_dir, 'gn']
+    if self._sanitizer:
+      out_dir.append(self._sanitizer)
+
+    if use_flavor:
+      return os.path.join('_'.join(out_dir), self.build_flavor_dir)
+
+    return '_'.join(out_dir)
+
+  def _generate_coverage_args(self, test_list):
+    """Generates unit test related arguments for the coverage script.
+
+       The coverage script takes in a list of unit test binaries as
+       an argument, and a corresponding test command for that binary.
+       This function reads from a list of test commands and constructs
+       a list of binaries and the command for that binary to be parsed
+       by coverage.
+
+    Args:
+      test_list: A list where each element represents a test command.
+
+    Returns:
+      A list of binaries and a list of commands arguments.
+    """
+    test_binary_list = []
+    test_command_list = []
+
+    for test_binary_command in test_list:
+      test_command = test_binary_command.strip().split(' ')
+      test_name = test_command[0]
+      if test_name in TESTS_TO_FILTER_OUT:
+        # Skip filtered tests
+        continue
+      test_binary_list.append(test_name)
+      test_command_list.append('--command')
+      test_command_list.append(
+          self._construct_formatted_test_command(test_command))
+
+    return test_binary_list, test_command_list
+
+  def _construct_formatted_test_command(self, test_command):
+    """Formats a test command to be parsed by the coverage script.
+
+    Args:
+      test_command: A list representation of a test command.
+
+    Returns:
+      String argument representation of test command for the coverage script.
+    """
+    test_path = os.path.join(self._get_build_out_dir(use_flavor=True),
+                             test_command[0])
+    return '{} {}'.format(test_path, ' '.join(test_command[1:]))
+
+  def _get_test_list(self):
+    return self.get_step_data(STEP_DATA_TEST_LIST)
+
+  def get_lcov_output_file_paths(self):
+    return [pathlib.Path(self.get_gcs_dir()) / COVERAGE_LCOV_PATH]
+
+  def run(self):
+    """Runs the chromium coverage script and generates a report.
+
+    Returns:
+      True iff there were no errors.
+    """
+    # JUnit tests are not supported.
+    if self._junit:
+      return True
+    else:
+      test_list = self._get_test_list()
+
+    test_binary_list, test_command_list = self._generate_coverage_args(
+        test_list)
+
+    coverage_script_path = os.path.join(self.get_project_path('chromium/src'),
+                                        COVERAGE_SCRIPT_PATH)
+    vpython3_path = os.path.join(
+        self.get_project_path('chromium/tools/depot_tools'), 'vpython3')
+    report_path = os.path.join(self.get_gcs_dir(), COVERAGE_REPORT_PATH)
+
+    command = [vpython3_path, coverage_script_path]
+    command += test_binary_list
+    command += [
+      '-o', report_path, '-b', self._get_build_out_dir(use_flavor=True), '-v']
+    if self._format:
+      command += ['--format', self._format]
+    if self._ignore_filename_regex:
+      command += ['--ignore-filename-regex', self._ignore_filename_regex]
+    command += test_command_list
+
+    # This is necessary because generating the HTML for code coverage can
+    # take 3+ hours, in which there is no stdout printed at all. Since a
+    # build is killed if there is no stdout for 2 hours, this block outputs
+    # every 1 minute to keep the build alive.
+    # TODO(kevinmk): Work out a way to have the coverage command output
+    # something meaningful during the HTML generation step.
+    subprocess, log_fd, log_file_path = self.exec_non_blocking_subprocess(
+        command, env=self._test_env)
+
+    logging.info('Writing to FD: %d and path %s', log_fd, log_file_path)
+
+    while subprocess.poll() is None:
+      logging.info('[%s] Waiting for completion...',
+                   time.strftime('%Y-%m-%d %H:%M:%S %Z'))
+      try:
+        out_files = os.listdir(report_path)
+        logging.info('Generated Files: %s', str(out_files))
+      except FileNotFoundError:
+        pass  # Ignore the error if the folder hasn't been created yet.
+      time.sleep(60)
+
+    try:
+      shutil.copyfile(log_file_path, os.path.join(self.get_gcs_dir(),
+                                                  'coverage.py.log'))
+      with open(log_file_path, 'r') as f:
+        print(f.read())
+    except IOError:
+      logging.warning('Log file does not exist.')
+
+    return True
+
+
+class CollectLcovCoverageStep(base_step.BaseStep):
+  """Collects and alters the LCOV files for uploading as artifacts."""
+
+  def __init__(self, lcov_files, eureka_root, **kwargs):
+    super().__init__('Collect LCOV Coverage', eureka_root=eureka_root, **kwargs)
+    self._lcov_files = [pathlib.Path(f) for f in lcov_files]
+    self._workspace_root = pathlib.Path(eureka_root)
+
+  def run(self):
+    gcs_dir = pathlib.Path(self.get_gcs_dir())
+    artifact_directory = gcs_dir / 'coverage'
+    artifact_directory.mkdir(exist_ok=True)
+    lcov_output_property = []
+    for lcov_file in self._lcov_files:
+      output_filename = _transform_file_path(lcov_file)
+      output_path = artifact_directory / output_filename
+      logging.info('Copying %s to %s', lcov_file, output_path)
+      coverage_utils.rewrite_file_paths_in_lcov(
+          lcov_file, output_path, self._workspace_root)
+      lcov_output_property.append(str(output_path.relative_to(gcs_dir)))
+    if lcov_output_property:
+      self.set_build_property('coverage_lcov_files', lcov_output_property)
+    return True
+
+
+def _transform_file_path(file_path):
+  """Constructs a unique filename from path."""
+  return '_'.join(file_path.resolve().relative_to('/').parts)
diff --git a/cq/scripts/slave/step/combined_sdk_clean.py b/cq/scripts/slave/step/combined_sdk_clean.py
new file mode 100644
index 0000000..3c21d8b
--- /dev/null
+++ b/cq/scripts/slave/step/combined_sdk_clean.py
@@ -0,0 +1,85 @@
+"""Build step class for setting up a clean SDK build environment."""
+
+from __future__ import absolute_import
+from slave.step import shell_step
+
+
+_REPO_CMD = ['repo', '--time']
+
+
+def _dirty_validator(returncode, stdout, stderr):
+  return returncode == 0 and 'dirty' not in stdout and 'dirty' not in stderr
+
+
+class CleanPrebuiltsSDKStep(shell_step.ShellStep):
+  """Build step class for setting up a clean SDK build environment."""
+
+  def __init__(self, purge_dirs, **kwargs):
+    """Creates a CleanSDKStep instance."""
+    shell_step.ShellStep.__init__(
+        self,
+        name='SDK: clean build output & prebuilts',
+        halt_on_failure=True,
+        num_retries=0,
+        **kwargs)
+    self._purge_dirs = purge_dirs
+
+  def get_commands(self):
+    commands = []
+    if self._purge_dirs:
+      commands += [self.ShellCommand(['rm', '-rf'] + self._purge_dirs)]
+    return commands
+
+
+class DirtyCheckSDKStep(shell_step.ShellStep):
+  """Build step class for setting up a clean SDK build environment."""
+
+  def __init__(self, halt_on_failure=True, **kwargs):
+    """Creates a CleanSDKStep instance."""
+    name = 'SDK: git rev dirty '
+    name += 'check' if halt_on_failure else 'info'
+    shell_step.ShellStep.__init__(
+        self,
+        name=name,
+        halt_on_failure=halt_on_failure,
+        num_retries=0,
+        **kwargs)
+    self._repo_regex = ['-r', kwargs['repo_regex']
+                       ] if 'repo_regex' in kwargs else []
+
+  def get_commands(self):
+    return [
+        self.ShellCommand(
+            _REPO_CMD + [
+                'forall', *self._repo_regex, '-p', '--verbose', '--command',
+                'git rev-parse --verify --short HEAD'
+            ],
+            validator=_dirty_validator),
+    ]
+
+
+class CleanCheckoutSDKStep(shell_step.ShellStep):
+  """Build step class for setting up a clean SDK build environment."""
+
+  def __init__(self, **kwargs):
+    """Creates a CleanSDKStep instance."""
+    shell_step.ShellStep.__init__(
+        self,
+        name='SDK: reset & clean',
+        halt_on_failure=True,
+        num_retries=0,
+        **kwargs)
+    self._repo_regex = ['-r', kwargs['repo_regex']
+                       ] if 'repo_regex' in kwargs else []
+
+  def get_commands(self):
+    return [
+        self.ShellCommand(_REPO_CMD + [
+            'forall', *self._repo_regex, '-p', '--verbose', '--command',
+            'git reset --hard'
+        ]),
+        self.ShellCommand(_REPO_CMD + [
+            'forall', *self._repo_regex, '-p', '--verbose', '--command',
+            'git clean -fdx'
+        ]),
+    ]
diff --git a/cq/scripts/slave/step/combined_sdk_compile.py b/cq/scripts/slave/step/combined_sdk_compile.py
new file mode 100644
index 0000000..3d431d3
--- /dev/null
+++ b/cq/scripts/slave/step/combined_sdk_compile.py
@@ -0,0 +1,141 @@
+"""Build step class for compiling SDK."""
+
+from __future__ import absolute_import
+import os
+
+from slave.step import shell_step
+
+
+class BaseSDKStep(shell_step.ShellStep):
+  """Base step class for SDK build."""
+
+  def __init__(self, name, build_config, **kwargs):
+    """Creates a BaseSDKStep instance."""
+    shell_step.ShellStep.__init__(
+        self,
+        name=name,
+        halt_on_failure=True,
+        num_retries=0,
+        **kwargs)
+    self._vendor_config = build_config['vendor_config']
+
+
+class OssCreateTarStep(BaseSDKStep):
+  """Tarball open source files step class for SDK build."""
+
+  def __init__(self, build_config, **kwargs):
+    """Creates a OssCreateTarStep instance."""
+    BaseSDKStep.__init__(self, name='SDK: tarball open source files',
+                         build_config=build_config, **kwargs)
+
+  def get_commands(self):
+    oss_sdk_dir = self._vendor_config['oss_sdk_dir']
+    oss_sdk_release_script = self._vendor_config['oss_sdk_release_script']
+    return [
+        self.ShellCommand(['mkdir', '-p', oss_sdk_dir]),
+        self.ShellCommand([oss_sdk_release_script, oss_sdk_dir]),
+    ]
+
+
+class CreateTarStep(BaseSDKStep):
+  """Tarball prebuilt file step class for SDK build."""
+
+  def __init__(self, build_config, **kwargs):
+    """Creates a CreateTarStep instance."""
+    BaseSDKStep.__init__(self, name='SDK: tar-ing build output',
+                         build_config=build_config, **kwargs)
+
+  def get_commands(self):
+    prebuilt_file = self._vendor_config['prebuilt_file']
+    fake_work_dir = self._vendor_config['fake_work_dir']
+    return [self.ShellCommand(['tar', 'czf',
+                               prebuilt_file,
+                               '-C', fake_work_dir, '.'])]
+
+
+class CreateLogDirStep(BaseSDKStep):
+  """Tarball prebuilt file step class for SDK build."""
+
+  def __init__(self, build_config, **kwargs):
+    """Creates a CreateLogDirStep instance."""
+    BaseSDKStep.__init__(self, name='SDK: ensure sdk log dir structure exists',
+                         build_config=build_config, **kwargs)
+
+  def get_commands(self):
+    build_logs_dir = self._vendor_config['build_logs_dir']
+    return [self.ShellCommand(['mkdir', '-p', build_logs_dir])]
+
+
+class ArchiveArtifactsStep(BaseSDKStep):
+  """Copy SDK build artifacts to upload."""
+
+  def __init__(self, build_config, **kwargs):
+    """Creates a ArchiveArtifactsStep instance."""
+    BaseSDKStep.__init__(self, name='SDK: archive artifacts',
+                         build_config=build_config, **kwargs)
+
+  def get_commands(self):
+    commands = []
+    upload_dir = self.get_gcs_dir()
+
+    prebuilt_file = self._vendor_config['prebuilt_file']
+    commands += [self.ShellCommand(['cp', prebuilt_file, upload_dir])]
+
+    build_logs_dir = self._vendor_config['build_logs_dir']
+    if os.path.isdir(build_logs_dir) and os.listdir(build_logs_dir):
+      commands += [self.ShellCommand(['cp', '-r', build_logs_dir, upload_dir])]
+
+    oss_sdk_dir = self._vendor_config['oss_sdk_dir']
+    if os.path.isdir(oss_sdk_dir) and os.listdir(oss_sdk_dir):
+      commands += [self.ShellCommand(['cp', '-r', oss_sdk_dir, upload_dir])]
+
+    return commands
+
+
+class CompileSDKStep(BaseSDKStep):
+  """Build step class for compiling SDK build."""
+
+  def __init__(self, build_config, **kwargs):
+    """Creates a CompileSDKStep instance."""
+    name = 'SDK: compile {} SDK'.format(build_config['vendor_config']['name'])
+    BaseSDKStep.__init__(self, name=name,
+                         build_config=build_config, **kwargs)
+    self._product = build_config['product']
+
+  def get_commands(self):
+    commands = []
+
+    fake_prebuilt_dir = self._vendor_config['fake_prebuilt_dir'].format(
+        vendor=self._vendor_config['name'], product=self._product)
+    commands += [self.ShellCommand(['mkdir', '-p', fake_prebuilt_dir])]
+
+    build_script = self._vendor_config['build_script']
+    fake_work_dir = self._vendor_config['fake_work_dir']
+    build_command = [build_script, fake_work_dir]
+    if self._vendor_config['include_product_param']:
+      build_command.append(self._product)
+    build_command.append(self.build_number)
+    build_command.append(
+        '--venv_root={}'.format(self.get_step_data('virtual_env_root')))
+    commands += [self.ShellCommand(build_command)]
+    return commands
+
+
+class CopySDKStep(BaseSDKStep):
+  """Build step class for copying the built SDK output"""
+
+  def __init__(self, build_config, **kwargs):
+    """Creats a CopySDKStep instance."""
+    name = 'SDK: copy {} SDK to eureka'.format(build_config.get('product'))
+    self._eureka_root = kwargs['eureka_root']
+    self._build_config = build_config
+    BaseSDKStep.__init__(
+        self, name=name, build_config=build_config, **kwargs)
+
+  def get_commands(self):
+    fake_work_dir = self._vendor_config['fake_work_dir']
+    return [
+        self.ShellCommand(
+            ['cp', '-dprv', os.path.join(fake_work_dir, '.'),
+             str(self._eureka_root)])
+    ]
diff --git a/cq/scripts/slave/step/compdb_gen_step.py b/cq/scripts/slave/step/compdb_gen_step.py
new file mode 100644
index 0000000..4751646
--- /dev/null
+++ b/cq/scripts/slave/step/compdb_gen_step.py
@@ -0,0 +1,92 @@
+"""
+  Build step class for generating compilation database from build directory,
+  it is useful for analysis tools that require actual compilation command,
+  e.g. clang-tidy and iwyu
+"""
+
+from __future__ import absolute_import
+import logging
+import os
+
+from helpers import commit_message_utils
+from helpers import diff_utils
+from helpers import git_utils
+from slave import base_step
+
+BUILD_SCRIPT_PROJECT = 'chromecast/internal'
+BUILD_SCRIPT_REL_PATH = 'build/build_chromecast.py'
+
+
+class CompdbGenStep(base_step.BaseStep):
+  """Build step class for generating compilation database"""
+
+  def __init__(self, build_dir, **kwargs):
+    """Creates a CompdbGenStep instance.
+
+    Args:
+      build_dir: The relative path that contains the build. (relative to
+        chromium/src project path)
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='compdb_gen', **kwargs)
+    self._build_dir = build_dir
+
+  def run(self):
+    """ Use gn to generate compilation database
+
+    Returns:
+      True iff success.
+    """
+
+    # Make sure build directory exists before using it to generate
+    # compilation database. (because the next call to exec_subprocess_chain
+    # can fail silently if the first command files)
+    returncode, _, _ = self.exec_subprocess(
+        ['ls', self._build_dir], cwd=self.get_project_path('chromium/src'))
+
+    if returncode != 0:
+      logging.info(
+          'Build directory does not exist. May need to update the build_dir '
+          'argument passed into this step.')
+      return False
+
+    compdb_gen_command = ([
+        'ninja', '-C', self._build_dir, '-t', 'compdb', 'cxx', 'cc'
+    ], {
+        'cwd': self.get_project_path('chromium/src')
+    })
+
+    write_to_file = (['cp', '/dev/stdin', 'compile_commands.json'], {
+        'cwd': self.get_project_path('chromium/src')
+    })
+
+    _, _, _ = self.exec_subprocess_chain([compdb_gen_command, write_to_file])
+
+    # Remove rbe specific parameters from compilation database, which
+    # present before the path of clang++ compiler. This is not required but
+    # can reduce unnecessary warnings output by clang_tidy/iwyu
+    # pylint: disable=line-too-long
+    patch_rbe_command = [
+        'sed', '-i',
+        's|"command":.*\.\./\.\./third_party/llvm-build/Release+Asserts/bin/clang++'
+        ' |"command": '
+        '"../../third_party/llvm-build/Release+Asserts/bin/clang++ |g',
+        'compile_commands.json'
+    ]
+
+    _, _, _ = self.exec_subprocess(
+        patch_rbe_command, cwd=self.get_project_path('chromium/src'))
+
+    # Do the same for clang
+    # pylint: disable=line-too-long
+    patch_rbe_command = [
+        'sed', '-i',
+        's|"command":.*\.\./\.\./third_party/llvm-build/Release+Asserts/bin/clang'
+        ' |"command": "../../third_party/llvm-build/Release+Asserts/bin/clang '
+        '|g', 'compile_commands.json'
+    ]
+
+    _, _, _ = self.exec_subprocess(
+        patch_rbe_command, cwd=self.get_project_path('chromium/src'))
+
+    return True
diff --git a/cq/scripts/slave/step/device_unit_test_step.py b/cq/scripts/slave/step/device_unit_test_step.py
new file mode 100644
index 0000000..b4c4fb7
--- /dev/null
+++ b/cq/scripts/slave/step/device_unit_test_step.py
@@ -0,0 +1,24 @@
+"""Override ShellStep to set cwd and env."""
+
+from __future__ import absolute_import
+from slave.step import shell_step
+
+
+class UnitTestShellStep(shell_step.ShellStep):
+  """Unit test steps which include cwd and env."""
+
+  def __init__(self, env=None, **kwargs):
+    """Constructor."""
+    shell_step.ShellStep.__init__(self, **kwargs)
+    self._env = env
+
+  def get_commands(self):
+    """Returns which commands to be run. Can be overridden by a subclass.
+
+    Returns:
+      List of ShellCommand objects to execute.
+    """
+    assert isinstance(self._command, list)
+    return [self.ShellCommand(self._command,
+                              cwd=self.directory,
+                              env=self._env)]
diff --git a/cq/scripts/slave/step/dipper_step.py b/cq/scripts/slave/step/dipper_step.py
new file mode 100644
index 0000000..5bf0089
--- /dev/null
+++ b/cq/scripts/slave/step/dipper_step.py
@@ -0,0 +1,99 @@
+"""Build step class for Dipper build."""
+
+from __future__ import absolute_import
+import os
+
+from slave import base_step
+
+class DipperStep(base_step.BaseStep):
+  """Build step class for Dipper build."""
+
+  def __init__(self, build_name, name='Dipper build', **kwargs):
+    """Create a Dipper instance.
+
+    Args:
+      build_name: Name of build (e.g. dipper-eng).
+      name: User-visible name of this step.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name=name, **kwargs)
+    self._build_name = build_name
+
+  def _get_pip_local_path(self):
+    """Gets the local python package folder.
+
+    Returns:
+      The local dipper-required python package folder as string.
+    """
+    gcs_path = self._properties.get(
+        'gcs_mirror_path', '/workspace/gcs-mirror')
+    return os.path.expanduser(os.path.join(gcs_path,
+                                           'team-dipper/python_packages'))
+
+  def _build(self, variant, src_dir):
+    """Build |build_name| artifacts by Dipper toolchain.
+
+    Args:
+      variant: "user" or "eng" variant
+      src_dir: the root path of source codes
+
+    Returns:
+      True if artifacts were built successfully; False if failed.
+    """
+    os.environ['PIP_FIND_LINKS'] = self._get_pip_local_path()
+
+    build_command = [
+        os.path.join('build', 'build.sh'),
+        '--{variant}'.format(variant=variant),
+    ]
+    returncode, stdout, stderr = self.exec_subprocess(
+        build_command, cwd=src_dir)
+    if returncode != 0:
+      self._error_processor(stdout, stderr)
+      return False
+
+    return True
+
+  def _copy(self, variant, src_dir):
+    """Copy |build_name| artifacts to GCS.
+
+    Args:
+      variant: "user" or "eng" variant
+      src_dir: the root path of source codes
+
+    Returns:
+      True if artifacts were copied successfully; False if failed.
+    """
+
+    artifacts_path = 'out/target/product/dipper'
+    artifacts_file = os.path.join(
+        artifacts_path, 'd6_artifacts-{variant}.zip'.format(variant=variant))
+    dist_dir = self.get_gcs_dir()
+    copy_command = [
+        'cp',
+        artifacts_file,
+        dist_dir
+    ]
+    returncode, stdout, stderr = self.exec_subprocess(copy_command, cwd=src_dir)
+    if returncode != 0:
+      self._error_processor(stdout, stderr)
+      return False
+
+    return True
+
+  def run(self):
+    """Build artifacts and copy to distribution(GCS).
+
+    Returns:
+      True if there were no errors.
+    """
+    variant = "user" if self._build_name.endswith("user") else "eng"
+    src_dir = self.get_project_path('telink/d6')
+
+    return self._build(variant, src_dir) and self._copy(variant, src_dir)
+
+  def _error_processor(self, stdout, stderr):
+    if stdout:
+      self.add_review({'message': stdout})
+    if stderr:
+      self.add_review({'message': stderr})
diff --git a/cq/scripts/slave/step/docker_push_testing_step.py b/cq/scripts/slave/step/docker_push_testing_step.py
new file mode 100644
index 0000000..b6de619
--- /dev/null
+++ b/cq/scripts/slave/step/docker_push_testing_step.py
@@ -0,0 +1,66 @@
+"""Step class for uploading new Docker images for testing team."""
+
+from __future__ import absolute_import
+import logging
+import os
+
+from slave import base_step
+
+class DockerPushTestingException(Exception):
+    """Raised when an error occurs with DockerPushTestingStep."""
+
+
+class DockerPushTestingStep(base_step.BaseStep):
+  """Uploads the latest Docker image."""
+
+  def __init__(self, bucket, path_info, **kwargs):
+    """ Creates a DockerPushTestingStep instance.
+
+    Args:
+      bucket: The URL of the docker bucket where the image will be pushed and
+      stored
+      path_info: Dictionary to have the project and internal path to the
+      Dockerfile. For example:
+        'path_info': {
+          'project': 'project_repo',
+          'dockerfile': 'path/to/dockerfile/folder'
+        }
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self,
+                                name='push home-testing Docker image',
+                                **kwargs)
+    self._docker_bucket = bucket
+    self._dockerfile_path = os.path.join(
+        self.get_project_path(path_info['project']),
+        path_info['dockerfile'])
+
+  def _push_image(self):
+    """Pushes the image to GCR.
+
+    Returns:
+    True if push succeeds, False otherwise
+    """
+    cmd = [
+      'cd',
+      self._dockerfile_path,
+      ';',
+      'docker',
+      'push',
+      self._docker_bucket]
+    return_code, _, stderr = self.exec_subprocess(cmd)
+    if return_code != 0:
+      logging.error('Failed to push image: %s', stderr)
+      return False
+    return True
+
+  def run(self):
+    """Push the build Docker image.
+
+    Returns:
+    True if successfull, else False.
+    """
+    success = self._push_image()
+    if not success:
+      self.add_review({'message': 'Failed to push the image'})
+    return  success
diff --git a/cq/scripts/slave/step/docker_testing_image_step.py b/cq/scripts/slave/step/docker_testing_image_step.py
new file mode 100644
index 0000000..5072aed
--- /dev/null
+++ b/cq/scripts/slave/step/docker_testing_image_step.py
@@ -0,0 +1,78 @@
+"""Build step class for building home testing docker image."""
+
+from __future__ import absolute_import
+import logging
+import os
+
+from slave import base_step
+
+
+class DockerImageError(Exception):
+  """Raised on step exceptions."""
+
+
+class DockerImageBuildStep(base_step.BaseStep):
+  """Steps for building docker testing image."""
+
+  def __init__(self, bucket, path_info, **kwargs):
+    """Creates a DockerTestingImageBuildStep instance.
+
+    Args:
+      bucket: The URL of the docker bucket where the image will be pushed and
+      stored
+      path_info: Dictionary to have the project and internal path to the
+      Dockerfile. For example:
+        'path_info': {
+          'project': 'project_repo',
+          'dockerfile': 'path/to/dockerfile/folder'
+        }
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self,
+                                name='docker_testing_image',
+                                **kwargs)
+    self._docker_bucket = bucket
+    self._dockerfile_path = os.path.join(
+        self.get_project_path(path_info['project']),
+        path_info['dockerfile'])
+
+  def _build_image(self):
+    """Builds and tags the image.
+
+    Returns:
+      If successfully built returns True, else False.
+    """
+    build_tag = ':{}'.format(self.build_number)
+
+    cmd = [
+        'cd',
+        self._dockerfile_path,
+        ';',
+        'docker',
+        'build',
+        '-t',
+        self._docker_bucket,
+        '-t',
+        self._docker_bucket + build_tag,
+        '.']
+
+    return_code, _, stderr = self.exec_subprocess(cmd)
+
+    if return_code != 0:
+      logging.error(
+          'Failed to build or tag Docker image:\n%s', stderr)
+      return False
+    return True
+
+  def run(self):
+    """Creates the Docker image and makes the push to GCR.
+
+    Sets a build property "review" with the output in case of failure.
+
+    Returns:
+        True if there were no test errors, False otherwise.
+    """
+    success = self._build_image()
+    if not success:
+      self.add_review({'message': 'Failed to build Docker image.'})
+    return success
diff --git a/cq/scripts/slave/step/fetch_ota_step.py b/cq/scripts/slave/step/fetch_ota_step.py
new file mode 100644
index 0000000..f1de0af
--- /dev/null
+++ b/cq/scripts/slave/step/fetch_ota_step.py
@@ -0,0 +1,100 @@
+"""Fetch OTA step."""
+
+from __future__ import absolute_import
+import importlib
+import logging
+import os
+import sys
+
+# As per b/128461961 requests is an indirect requirement to import fetcher
+import requests
+from slave import base_step
+
+
+class FetchOtaException(Exception):  # pylint: disable=g-bad-exception-name
+  """Raised when a fetch_ota_step exception happens."""
+
+
+class FetchOtaStep(base_step.BaseStep):
+  """Step to fetch the required OTA for a device."""
+
+  def __init__(
+      self, branch, product, board,
+      flavor, work_dir, **kwargs):
+    """Creates a FetchOtaStep instance.
+
+    Args:
+      branch: Which branch should be used.
+      product: Name of DUT.
+      board: Type of board of product.
+      flavor: Type of build.
+      work_dir: Location of files to work on.
+      **kwargs: Any additionar args to pass to BaseStep
+    """
+    base_step.BaseStep.__init__(self, name='fetch_ota', **kwargs)
+    self._branch = branch
+    self._board_type = board
+    self._flavor = flavor
+    self._product = product
+    self._work_dir = work_dir
+
+  def _get_fetcher_module(self):
+    """Returns the fetcher class from eurtest.
+
+    Returns:
+      Fetcher module from eurtest.
+    """
+    test_path = os.path.abspath(os.path.join(
+        os.pardir, self.get_project_path('test')))
+    if test_path not in sys.path:
+      sys.path.insert(
+          0, test_path)
+    return importlib.import_module('eurtest.tools.build.fetcher')
+
+  def GetFetcher(self):
+    """Configures the fetcher for the OTA required.
+
+    Returns:
+      A Fetcher instance for the OTA required for this test run.
+    """
+    fetcher = self._get_fetcher_module()
+    ota_fetcher = fetcher.GetFetcher(
+        branch=self._branch,
+        product=self._product,
+        flavor=self._flavor,
+        board_type=self._board_type
+    )
+    return ota_fetcher
+
+  def _fetch(self):
+    """Fetches the OTA using the recipe information passed down.
+
+    Returns:
+       If successfully fetched returns True, else False.
+    """
+    success = True
+    fetcher_lib = self._get_fetcher_module()
+    ota_fetcher = self.GetFetcher()
+    try:
+      ota_fetcher.Fetch(self.get_property('build_number'), self._work_dir)
+    except fetcher_lib.FetcherError:
+      success = False
+      logging.error(
+          'Failed to found ota for build %s for %s',
+          self.get_property('build_number'),
+          self._product)
+    return success
+
+  def run(self):
+    """Fetches and saves in a temporary directory the OTA.
+
+    Returns:
+      True if successfully fetched the OTA, False otherwise.
+    """
+    success = self._fetch()
+    if not success:
+      review_message = 'Failed to fetch OTA for build %s and device %s' % (
+          self.get_property('build_number'),
+          self._product)
+      self.add_review({'message': review_message})
+    return success
diff --git a/cq/scripts/slave/step/fuchsia_cipd_upload_step.py b/cq/scripts/slave/step/fuchsia_cipd_upload_step.py
new file mode 100644
index 0000000..fab461f
--- /dev/null
+++ b/cq/scripts/slave/step/fuchsia_cipd_upload_step.py
@@ -0,0 +1,55 @@
+"""Step class for uploading fuchsia-specific CIPD artifacts."""
+
+from __future__ import absolute_import
+import os
+
+from slave import base_step
+from slave.step import cipd_upload_step
+
+class FuchsiaCIPDUploadStep(base_step.BaseStep):
+    """Generates all required CIPDUploadSteps for a fuchsia build."""
+
+    def __init__(self, recipe_steps, file_name, yaml_dir, refs=None, tags=None,
+                 pkg_vars=None, **kwargs):
+        super().__init__(name='fuchsia_cipd_upload', **kwargs)
+        self.recipe_steps = recipe_steps
+        self.file_name = file_name
+        self.yaml_dir = yaml_dir
+
+        # Assert that required CIPD kwargs have been passed in. These
+        # will be forworded to CIPDUploadStep.
+        assert refs is not None
+        assert tags is not None
+        assert pkg_vars is not None
+        self.cipd_kwargs = (
+            kwargs | { 'refs': refs, 'tags': tags, 'pkg_vars': pkg_vars }
+        )
+
+    def run(self):
+        # Assume that there is only one instance of this step in
+        # |self._recipe_steps|.
+        current_step_index = next(
+            (i for i, step in enumerate(self.recipe_steps)
+             if isinstance(step, FuchsiaCIPDUploadStep)), -1)
+        if current_step_index == -1:
+            raise RuntimeError(
+                'Could not find FuchsiaCIPDUploadStep class in recipe')
+
+        contents = []
+        with open(self.file_name) as f:
+            contents = f.readlines()
+
+        # The file locations stored in CIPD_UPLOADS_FILE_NAME are relative
+        # to chromium root and prefixed with "//". We strip the "//" prefix
+        # so that the remaining relative path can be easily joined.
+        for artifact in [line.strip().lstrip('/') for line in contents]:
+            artifact_yaml = os.path.basename(artifact)
+            cipd_far_def = os.path.join(self.yaml_dir, artifact)
+            step = cipd_upload_step.CIPDUploadStep(
+                cipd_far_def,
+                step_name='Upload .far CIPD Package: ' + artifact_yaml + '.',
+                verification_timeout='10m0s',
+                **self.cipd_kwargs)
+            # Add the step after the current step, FuchsiaCIPDUploadStep.
+            self.recipe_steps.insert(current_step_index + 1, step)
+        return True
diff --git a/cq/scripts/slave/step/fuchsia_emulator_step.py b/cq/scripts/slave/step/fuchsia_emulator_step.py
new file mode 100644
index 0000000..6a22d6b
--- /dev/null
+++ b/cq/scripts/slave/step/fuchsia_emulator_step.py
@@ -0,0 +1,237 @@
+"""Step class for managing a Fuchsia emulator instance using ffx emu."""
+
+import logging
+import os
+import shutil
+import socket
+
+from unittest import mock
+
+from slave import base_step
+
+
+_STEP_DATA_FEMU_CAST = 'step_data_femu_cast'
+_STEP_DATA_FEMU_HTTP = 'step_data_femu_http'
+_STEP_DATA_FEMU_HTTPS = 'step_data_femu_https'
+_STEP_DATA_FEMU_SSH = 'step_data_femu_ssh'
+_STEP_DATA_FEMU_LOG = 'step_data_femu_log'
+_STEP_DATA_PACKAGE_SERVER_LOG = 'step_data_package_server_log'
+
+_FFX_LOG = 'ffx.log'
+_FFX_DAEMON_LOG = 'ffx.daemon.log'
+
+_SSH_CONFIG_TEMPLATE = '''
+# Generated by Eureka CQ FuchsiaEmulatorStep using ffx emu.
+# Ports are changed to an available TCP port assigned to QEMU.
+Host *
+  Port {port}
+  StrictHostKeyChecking no
+  IdentityFile {auth_path}
+  ControlPersist 1m
+  ControlMaster auto
+  ControlPath /tmp/ssh-%r@%h:%p
+  ServerAliveInterval 1
+  ServerAliveCountMax 3
+  ConnectTimeout 5
+'''
+
+class FuchsiaEmulatorError(RuntimeError):
+  """Errors relating to FEMU."""
+
+class BaseFuchsiaEmulatorStep(base_step.BaseStep):
+  """BaseFuchsiaEmulatorStep with common utilities."""
+  def __init__(self,
+               fuchsia_sdk_root,
+               fuchsia_workdir,
+               product='terminal.qemu-x64',
+               name='Unknown FEMU step',
+               **kwargs):
+    """Creates a BaseFuchsiaEmulatorStep instance.
+
+    Args:
+      fuchsia_sdk_root: Path to the Fuchsia SDK.
+      fuchsia_workdir: Path to directory with Fuchsia artifacts.
+      product: Product image to run the emulator on.
+      name: Name of the step to pass to BaseStep.
+      kwargs: Remaining args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name=name, **kwargs)
+    self._fuchsia_sdk_root = fuchsia_sdk_root
+    prod, board = product.split('.', 1)
+    self._product_bundle = os.path.join(fuchsia_sdk_root, 'images', prod, board)
+    # GN SDK scripts
+    self._ffx = os.path.join(self._fuchsia_sdk_root, 'sdk/tools/x64/ffx')
+
+    # Common Paths
+    self._fuchsia_workdir_path = fuchsia_workdir
+
+  def _dump_file_to_log(self, file_path):
+    """Dump file to log.
+
+    Args:
+        file_path: Location of the file to add to the logger output.
+    """
+    logging.info('%s log:', file_path)
+    with open(file_path) as file_fd:
+      logging.info(file_fd.read())
+
+
+class FuchsiaEmulatorStartStep(BaseFuchsiaEmulatorStep):
+  """Step for starting local Fuchsia assets on FEMU."""
+
+  def __init__(self, **kwargs):
+    """Creates a FuchsiaEmulatorCreateStep instance.
+
+    Args:
+      kwargs: Passed to FuchsiaEmulatorBaseStep.
+    """
+    BaseFuchsiaEmulatorStep.__init__(self, name="Start FEMU", **kwargs)
+    self.femu_log = None
+    self.package_server_log = None
+    self._ports_in_use = []
+
+  def _get_available_tcp_port(self, max_attempts=100):
+    """Find a unique and open port.
+
+    Verify port is available by opening a listening socket on it.
+
+    Verify uniqueness by storing a list of ports returned, and ensuring no
+    duplicates in this list.
+    """
+    # Search for a port that is open and verify it isn't
+    # already being used for another FEMU port.
+    attempts = 0
+    while attempts < max_attempts:
+      attempts += 1
+      # Look for an open port
+      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+      sock.bind(("", 0))
+      port = sock.getsockname()[1]
+      sock.close()
+      # Verify port is not being used for another port
+      if port not in self._ports_in_use:
+        break
+    if attempts >= max_attempts:
+      raise FuchsiaEmulatorError('Failed to find open tcp port')
+    self._ports_in_use.append(port)
+    return port
+
+  def write_file(self, file, content):
+    with open(file, 'w') as f:
+      f.write(content)
+
+  def create_ssh_config(self):
+    """Create SSH config for the test runner to connect to FEMU.
+
+    Assumes that FEMU has already been started.
+    """
+    home_dir = os.path.expanduser('~')
+    ssh_dir = os.path.join(home_dir, '.ssh')
+    returncode, _, _ = self.exec_subprocess(['mkdir', '-p', ssh_dir])
+    if returncode != 0:
+      self._dump_file_to_log(self.femu_log)
+      self._dump_file_to_log(self.package_server_log)
+      raise FuchsiaEmulatorError('Failed to create ssh out dir')
+    ssh_config_path = os.path.join(home_dir, '.fuchsia', 'sshconfig')
+    ssh_config_content = _SSH_CONFIG_TEMPLATE.format(
+      port=self._ssh_port,
+      auth_path=os.path.join(ssh_dir, 'fuchsia_ed25519'))
+    self.write_file(ssh_config_path, ssh_config_content)
+    logging.info('ssh_config for chromium test at %s', ssh_config_path)
+
+  def start_femu(self):
+    """Start Fuchsia Emulator process."""
+    self._ssh_port = self._get_available_tcp_port()
+
+    _, self.femu_log = self.get_new_tmp_file()
+    logging.info('femu log at %s', self.femu_log)
+    self.add_step_data(_STEP_DATA_FEMU_LOG, self.femu_log)
+
+    _, self.package_server_log = self.get_new_tmp_file()
+    logging.info('package server log at %s', self.package_server_log)
+    self.add_step_data(_STEP_DATA_PACKAGE_SERVER_LOG, self.package_server_log)
+
+    # HACK(b/290049007)
+    # For currently unknown reasons, there is an ffx daemon socket already
+    # present, but occassionally unresponsive. The emu start command will
+    # fail if it attempts to use this bad daemon. As a stop-gap measure
+    # to get Eureka CQ/CB back to green, we will force restart the daemon.
+    ffx_daemon_stop = [
+        self._ffx,
+        '-c', 'log.level=debug',
+        '-c', 'log.dir={}'.format(self._fuchsia_workdir_path),
+        'daemon',
+        'stop',
+        '-t', '5000',
+    ]
+    self.exec_subprocess(ffx_daemon_stop, check_output=True)
+
+    # Start FEMU
+    femu_command = [
+        self._ffx,
+        '--timeout', '100',
+        '-c', 'log.level=debug',
+        '-c', 'log.dir={}'.format(self._fuchsia_workdir_path),
+        'emu',
+        'start',
+        '--startup-timeout', '100',
+        self._product_bundle,
+        '--log', self.femu_log,
+        '--port-map', f'ssh:{self._ssh_port}',
+        '--headless',  # Run without gui on infra
+    ]
+    self.exec_subprocess(femu_command, check_output=True)
+
+  def run(self):
+    """Starts a local FEMU instance."""
+    self.start_femu()
+    # Copy SSH config needed for chromium test runners.
+    self.create_ssh_config()
+    # Output logs to give additional debugging information.
+    self._dump_file_to_log(self.femu_log)
+    # Store femu ports in step data for test steps.
+    self.add_step_data(_STEP_DATA_FEMU_SSH, self._ssh_port)
+    return True
+
+
+class FuchsiaEmulatorTeardownStep(BaseFuchsiaEmulatorStep):
+  """Step for cleaning up after the FuchsiaEmulatorStartStep."""
+
+  def __init__(self, **kwargs):
+    """Creates a FuchsiaEmulatorTeardownStep instance."""
+    BaseFuchsiaEmulatorStep.__init__(self, name="Teardown FEMU", **kwargs)
+
+  def _copy_logs_to_gcs(self):
+    """Copies FuchsiaEmulator related logs to the GCS directory"""
+    log_source_files_to_destinations = {
+        self.get_step_data(_STEP_DATA_FEMU_LOG): 'femu_system_log.txt',
+        self.get_step_data(
+            _STEP_DATA_PACKAGE_SERVER_LOG
+        ): 'package_server_log.txt',
+        os.path.join(self._fuchsia_workdir_path, _FFX_LOG): 'ffx.log',
+        os.path.join(
+            self._fuchsia_workdir_path, _FFX_DAEMON_LOG
+        ): 'ffx.daemon.log',
+    }
+
+    for src, dst in log_source_files_to_destinations.items():
+      if not os.path.isfile(src):
+        logging.warning('The expected log file "%s" does not exist!', src)
+        continue
+
+      gcs_upload_dst = os.path.join(self.get_gcs_dir(), dst)
+      shutil.copyfile(src, gcs_upload_dst)
+
+  def run(self):
+    """Cleanup after FuchsiaEmulatorTeardownStep."""
+    # Kill FEMU
+    self._copy_logs_to_gcs()
+    femu_command = [self._ffx,
+                    'emu',
+                    'stop',
+                    '--all',
+                    ]
+    # Don't check the return code. If emulator crashed, ffx emu will
+    # throw an error but it doesn't mean the kill action failed.
+    self.exec_subprocess(femu_command)
+    return True
diff --git a/cq/scripts/slave/step/fuchsia_hermetic_test_step.py b/cq/scripts/slave/step/fuchsia_hermetic_test_step.py
new file mode 100644
index 0000000..bbc3dcd
--- /dev/null
+++ b/cq/scripts/slave/step/fuchsia_hermetic_test_step.py
@@ -0,0 +1,25 @@
+"""Build step class for executing Fuchsia hermetic tests."""
+
+from __future__ import absolute_import
+import os
+
+from slave import base_step
+from slave.step import shell_step
+
+class FuchsiaHermeticTestStep(base_step.BaseStep):
+  """Build step class to run runner scripts as unit tests."""
+
+  def __init__(self, name='fuchsia_hermetic_tests', arch='x64', **kwargs):
+    base_step.BaseStep.__init__(self, name=name, **kwargs)
+    self._arch = arch
+
+  def run(self):
+    command = ['python3', os.path.join(self.get_project_path('standalone/src'),
+        'build/fuchsia/catabuilder_test_hook.py')]
+
+    command += ['--test_group_name', self.name]
+    command += ['--arch', self._arch]
+
+    returncode, _, _ = self.exec_subprocess(command)
+
+    return returncode == 0
diff --git a/cq/scripts/slave/step/fuchsia_unittest_step.py b/cq/scripts/slave/step/fuchsia_unittest_step.py
new file mode 100644
index 0000000..ba538ad
--- /dev/null
+++ b/cq/scripts/slave/step/fuchsia_unittest_step.py
@@ -0,0 +1,117 @@
+"""Build step class for executing unit tests."""
+
+from __future__ import absolute_import
+import logging
+import os
+
+from helpers import git_utils
+from slave import base_step
+from slave.step import shell_step
+
+class Error(RuntimeError):
+  """Error for this module."""
+
+
+class FuchsiaUnitTestStep(base_step.BaseStep):
+  """Build step class to run runner scripts as unit tests."""
+
+  def __init__(self, out_dir, fuchsia_workdir, recipe_steps,
+               name='fuchsia_unit_test', **kwargs):
+    base_step.BaseStep.__init__(self, name=name, **kwargs)
+    self._changed_files = None
+    self._fuchsia_workdir = fuchsia_workdir
+    self._out_dir = out_dir
+    self._step_kwargs = kwargs
+    self._recipe_steps = recipe_steps
+
+  def _current_step_index(self):
+    """Returns first index of this class in _recipe_steps."""
+    for step in self._recipe_steps:
+      # Assume there is only one instanceof this class.
+      if isinstance(step, FuchsiaUnitTestStep):
+        return self._recipe_steps.index(step)
+    return -1
+
+  def _gunit_retry_if(self, returncode, stdout, stderr):
+    """Returns whether or not the chromium unit test should be retried.
+
+    See more:
+      //chromium/src/build/fuchsia/runner_exceptions.py
+    """
+    # Test runner exits between [60, 80] for infra related flakes.
+    if 60 < returncode < 80:
+      return True
+    # Not all exceptions are caught by the test runner, and some require
+    # extra checks
+    reasons = [
+      # Package server failed to transfer blobs in time for test
+      "Error while resolving",
+      # Long running tests may experience the ssh connection dropping
+      "Timeout when executing",
+    ]
+    for retry_reason in reasons:
+      if retry_reason in stdout:
+        return True
+    return False
+
+
+  def add_unittest_step(self, suite_name, runner_cmd, fail_fast=False):
+    # 15 mins max timeout per test suite. Some tests like net_unittest
+    # take more than 5 mins to run.
+    max_timeout_secs = 15 * 60
+    # This step name makes it easy to filter Fuchsia test flakiness in the
+    # CQ build reliability dashboard
+    step_name = "Fuchsia Unittest %s" % suite_name
+    step = shell_step.ShellStep(
+        command=runner_cmd,
+        num_retries=2,
+        retry_if=self._gunit_retry_if,
+        directory=os.getcwd(),
+        halt_on_failure=fail_fast,
+        name=step_name,
+        timeout_secs=max_timeout_secs,
+        **self._step_kwargs)
+    # This step should be added after the current fuchsia unittest step.
+    # However, it is created during the recipe run, which is non-trivial.
+    current_step_index = self._current_step_index()
+    if current_step_index == -1:
+      raise Error('Could not find FuchsiaUnitTestStep class to append to')
+    self._recipe_steps.insert(current_step_index + 1, step)
+
+  def read_file(self, file_path):  # pylint: disable=method-hidden
+    """Reads a file.
+
+    Args:
+      file_path: A string with the path to the file open.
+
+    Returns:
+      A list of strings with the file content.
+    """
+    content_list = []
+    with open(file_path) as f:
+      content_list = f.readlines()
+    return [line.strip() for line in content_list]
+
+  def run(self):
+    test_list_path = os.path.join(os.getcwd(), self._out_dir, 'tests',
+                                  'fuchsia_run_test_list.txt')
+    runner_folder = os.path.join(os.getcwd(), self._out_dir, 'bin')
+    test_list = self.read_file(test_list_path)
+    for suite_cmd in test_list:
+      suite_cmd_parts = suite_cmd.split()
+      suite_name = suite_cmd_parts[0]
+
+      runner_script = 'run_{}'.format(suite_name)
+      fuchsia_out_dir = os.path.join(self._fuchsia_workdir, 'packages')
+      logs_dir = os.path.join(self.get_gcs_dir(), suite_name)
+      os.makedirs(logs_dir, exist_ok=True)
+
+      runner_cmd = [
+          os.path.join(runner_folder, runner_script),
+          '--logs-dir', logs_dir,
+          '--device', 'device',
+          '--fuchsia-out-dir', fuchsia_out_dir
+      ] + suite_cmd_parts[1:]  # Append gtest filters to the command
+
+      self.add_unittest_step(suite_name, runner_cmd, fail_fast=False)
+    return True
diff --git a/cq/scripts/slave/step/gcs_upload_step.py b/cq/scripts/slave/step/gcs_upload_step.py
new file mode 100644
index 0000000..5cc2177
--- /dev/null
+++ b/cq/scripts/slave/step/gcs_upload_step.py
@@ -0,0 +1,85 @@
+"""Build step class for uploading artifacts to GCS bucket."""
+from __future__ import absolute_import
+import logging
+import os
+
+from helpers import gcs_utils
+from helpers import repo_utils
+from slave import base_step
+
+
+class GcsUploadStep(base_step.BaseStep):
+  """Build step class for uploading artifacts to GCS bucket."""
+
+  def __init__(self, gcs_bucket, gcs_path, src_path=None, skip_empty=False,
+               name='gcs_upload', result_property='gcs_output', **kwargs):
+    """Creates a GcsUploadStep instance.
+
+    Step will upload from src_path to bigstore
+    The bigstore destination will be gs://<gcs_bucket>/<gcs_path>
+    If src_path pointing to dir, content of dir (e.g. file.txt) will be
+    uploaded to GCS as gs://<gcs_bucket>/<gcs_path>/file.txt
+    If src_path pointing to file (e.g. file.jpg) and gcs_path have trailing
+    slash, file will be uploaded gs://<gcs_bucket>/<gcs_path>/file.jpg
+    If src_path pointing to file (e.g. file.txt) and gcs_path does not have
+    trailing slash, than last component of gcs_path will become new name of
+    the uploaded object, file will be uploaded to gs://<gcs_bucket>/<gcs_path>
+
+    Args:
+      gcs_bucket: destination bucket name.
+      gcs_path: relative path in bucket to upload src_path.
+      src_path: local path to file or dir upload, default to get_gcs_dir().
+      skip_empty: if true step will not proceed and return success when
+                  src_path does not exists or epmty dir.
+      name: step name.
+      result_property: name of property to save upload location and result.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    self._src_path = src_path
+    self._skip_empty = skip_empty
+    self._gcs_bucket = gcs_bucket
+    self._gcs_path = gcs_path
+    self._result_property = result_property
+    base_step.BaseStep.__init__(self, name=name, **kwargs)
+
+  def run(self):
+    """Uploading to bigstore.
+
+    Returns:
+      True if upload completed successfully.
+    """
+
+    if not self._src_path:
+      self._src_path = self.get_gcs_dir()
+    if self._skip_empty:
+      if not os.path.exists(self._src_path):
+        logging.info('Path %s does not exist, skipping', self._src_path)
+        return True
+      if os.path.isdir(self._src_path) and not os.listdir(self._src_path):
+        logging.info('Directory %s is empty, skipping', self._src_path)
+        return True
+
+    if os.path.isdir(self._src_path):
+      path_to_upload = os.path.join(self._src_path, '.')
+      gcs_path = self._gcs_path
+    else:
+      path_to_upload = self._src_path
+      gcs_path = os.path.join(self._gcs_path, os.path.basename(self._src_path))
+
+    gcs_uri = 'gs://{gcs_bucket}/{gcs_path}'.format(
+        gcs_bucket=self._gcs_bucket, gcs_path=gcs_path)
+    command = gcs_utils.upload_directory_cmd(
+        path_to_upload, gcs_uri)
+    with repo_utils.normalize_manifest_if_exists(
+        self.get_property('manifest_override_path')):
+      returncode, _, _ = self.exec_subprocess(command)
+    gcs_output = {
+        'uri': gcs_uri,
+        'path': self._gcs_path,
+        'bucket': self._gcs_bucket,
+        'result': returncode,
+    }
+    self.set_build_property(self._result_property, gcs_output)
+    if returncode != 0:
+      return False
+    return True
diff --git a/cq/scripts/slave/step/git_clean_step.py b/cq/scripts/slave/step/git_clean_step.py
new file mode 100644
index 0000000..702c786
--- /dev/null
+++ b/cq/scripts/slave/step/git_clean_step.py
@@ -0,0 +1,46 @@
+"""Git clean step."""
+
+from __future__ import absolute_import
+import logging
+
+from helpers import branch_utils
+from slave import base_step
+
+
+class GitCleanStep(base_step.BaseStep):
+  """git clean step class."""
+
+  def __init__(self, **kwargs):
+    """Creates a GitCleanStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='git_clean', **kwargs)
+    self._clean_project = self.get_property('git_clean_project')
+    self._min_branch = self.get_property('git_clean_min_branch')
+
+  def run(self):
+    # Do all the cleaning.
+    command = ['git', 'clean', '-x', '-f', '-d']
+
+    if not branch_utils.is_branch_equal_to_or_later_than(
+        self.manifest_branch, self._min_branch):
+      logging.info('Branch is older than %s, nothing to do.', self._min_branch)
+      return True
+
+    logging.info("Clean project is '%s'", self._clean_project)
+    if not self._clean_project:
+      return True
+    projects = [p.strip() for p in self._clean_project.split(',')]
+
+    for project in projects:
+      if project:
+        clean_dir = self.get_project_path(project)
+      else:
+        clean_dir = None
+      logging.info("Cleaning project '%s' at '%s'", project, clean_dir)
+      returncode, _, _ = self.exec_subprocess(command, cwd=clean_dir)
+      if returncode != 0:
+        return False
+    return True
\ No newline at end of file
diff --git a/cq/scripts/slave/step/gn_check_step.py b/cq/scripts/slave/step/gn_check_step.py
new file mode 100644
index 0000000..bb8fd9e
--- /dev/null
+++ b/cq/scripts/slave/step/gn_check_step.py
@@ -0,0 +1,205 @@
+"""Build step class for running `gn check`."""
+import logging
+import os
+
+from helpers import git_utils
+from helpers import gn_utils
+from helpers import py_env_utils
+from slave import base_step
+
+_GN_ROOT_DIR_PROJECT = 'chromium/src'
+_GN_CHECK_OUT_DIR = 'out_gn_check'
+_MESH_TAG = '-mesh-eng'
+
+# Bare bones GN configs that allow for basic smoke tests to run during
+# presubmit. All the full builds also do GN checks as part of the build,
+# so the full GN configs will also be tested.
+_DESKTOP_VIDEO_GN_ARGS = ['is_chromecast=true',
+                          'chromecast_branding="internal"']
+_DESKTOP_AUDIO_GN_ARGS = ['is_chromecast=true',
+                          'chromecast_branding="internal"',
+                          'is_cast_audio_only=true']
+_GN_CONFIGS = [_DESKTOP_VIDEO_GN_ARGS,
+               _DESKTOP_AUDIO_GN_ARGS,]
+
+
+class GnCheckStepBase(base_step.BaseStep):
+  """Base step for running GnCheck.
+
+  The key part here being shared is the method for parsing gn check output
+  and making it into a comment.
+  """
+
+  def __init__(self, name:str='gn check', **kwargs):
+    super().__init__(name=name, **kwargs)
+
+  def run(self):
+    raise NotImplementedError
+
+  def gn_check_to_review(self, stdout):
+    """Converts gn_check |stdout| into comments and adds them to the review."""
+    comments = gn_utils.gn_check_error_to_comments(stdout)
+
+    # GN makes comments relative to the root GN directory,
+    # but we want the file comments to be relative to |self.directory|
+    directory_prefix = self.directory
+    gn_root_dir = self.get_project_path(_GN_ROOT_DIR_PROJECT)
+    if directory_prefix.startswith(gn_root_dir):
+      directory_prefix = directory_prefix[len(gn_root_dir + os.path.sep):]
+
+    # GN can generate comments for files not directly included in the CL, so
+    # split these comments out to the review message
+    files_in_cl = git_utils.changed_files(self, directory=self.directory)
+    file_comments, review_comments = {}, {}
+    for filename, comment_list in comments.items():
+      full_filename = filename
+      if directory_prefix and filename.startswith(directory_prefix):
+        filename = filename[len(directory_prefix + os.path.sep):]
+
+      if filename in files_in_cl:
+        file_comments[filename] = comment_list
+      else:
+        review_comments[full_filename] = comment_list
+
+      # To limit comment filesize, only add the first comment. In theory, seeing
+      # one GN check issue will inspire people to run gn check locally to clean
+      # up the other issues.
+      logging.info('All GN check comments: %s', comments)
+      break
+
+    # Generate the review message
+    review_message = [
+        'Run `gn check out_chromecast_$PRODUCT/release` or ',
+        '`lunch $PRODUCT && make build.ninja ENABLE_GN_CHECK=1`',
+    ]
+    for filename, comments in review_comments.items():
+      review_message.append(filename)
+      for comment in comments:
+        review_message.append('LINE: ' + comment['line'])
+        review_message.append(comment['message'])
+        review_message.append('----------')
+      review_message.append('')
+
+    self.add_review({
+        'message': '\n'.join(review_message),
+        'comments': file_comments
+    })
+
+
+class GnCheckStep(GnCheckStepBase):
+  """Build step class for running gn check via generate_gn helper."""
+
+  def __init__(self, name: str='gn check', **kwargs):
+    """Creates a GnCheckStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    super().__init__(name=name, **kwargs)
+
+  def run(self):
+    """Runs gn gen --check and reports any errors.
+
+    Sets a build property "review" with the output if there are errors.
+
+    Returns:
+      True iff gn check reported no errors.
+    """
+    out_dir = os.path.join(self.get_tmp_dir(), _GN_CHECK_OUT_DIR)
+    root_dir = self.get_project_path(_GN_ROOT_DIR_PROJECT)
+    all_checks_passed = True
+    for gn_config in _GN_CONFIGS:
+      command = [gn_utils.GN_BINARY,
+                 'gen', out_dir,
+                 '--root={}'.format(root_dir),
+                 '--args={}'.format(' '.join(gn_config)),
+                 '--check']
+      returncode, stdout, _ = self.exec_subprocess(command, quiet=True)
+
+      if returncode != 0:
+        all_checks_passed = False
+        self.gn_check_to_review(stdout)
+
+    return all_checks_passed
+
+
+class GnCheckForOta(GnCheckStepBase):
+  """Step for running gn check on OTA builds."""
+
+  def __init__(self, build_name, **kwargs):
+    """Creats a GnCheckForOta instance.
+
+    Args:
+      build_name: Name of build (e.g. pepperoni-eng, tvdefault-eng)
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    GnCheckStepBase.__init__(self, **kwargs)
+    self._enable_gn_check = gn_utils.is_gn_check_enabled(self.manifest_branch)
+    self._product = build_name.split('-')[0]
+    self._is_mesh_build = build_name.endswith(_MESH_TAG)
+    self._build_name = build_name.replace(_MESH_TAG, '-eng')
+
+  def rerun_if_necessary(self, returncode, stdout, stderr, command, env):
+    """Do some cleanup and rerun |command| if it failed previously.
+
+    In the majority of runs, this function will just be a no-op.
+
+    Occasionally, running `make` will produce a corrupted
+    AllocationTestHarness.P file. This bad file will stay around until it is
+    cleaned out, causing builds to fail unnecessarily. This checks for this case
+    and fixes it. (b/31939016)
+
+    Args:
+      returncode: Returncode from previous run of |command|
+      stdout: Stdout from previous run of |command|
+      stderr: Stderr from previous run of |command|
+      command: Command to potentially re-run (as a list of strings)
+      env: Extra environment variables to use
+
+    Returns:
+      (returncode, stdout, stderr) from |command|
+    """
+    if returncode != 0 and 'AllocationTestHarness.P' in stderr:
+      logging.warning('Bad AllocationTestHarness.P file was detected'
+                      ' (b/31939016). Cleaning and then re-trying the previous'
+                      ' command.')
+      clean_cmd = ['make', 'clean']
+      self.exec_subprocess(clean_cmd)
+
+      returncode, stdout, stderr = self.exec_subprocess(command, env=env)
+
+    return returncode, stdout, stderr
+
+  def run(self):
+    """"Builds the build.ninja target, passing --check to `gn gen`.
+
+    Returns:
+      True iff there were no gn check errors.
+    """
+    # `make PRODUCT-$product-build.ninja` is essentally equivalent to
+    # `lunch $product-eng && make build.ninja`. The build.ninja target
+    # then invokes `gn gen --check`.
+    command = [
+        'make', '-j{}'.format(self.get_num_jobs()),
+        'PRODUCT-' + self._product + '-build.ninja',
+        'ENABLE_GN_CHECK=1',
+        'DISABLE_AUTO_INSTALLCLEAN=true',
+    ]
+
+    if self._is_mesh_build:
+      command += ['USE_GWIFI=1']
+
+    command += self.build_accelerator.make_flags
+    env = self.build_accelerator.environment_variables
+
+    returncode, stdout, stderr = self.exec_subprocess(command, env=env)
+
+    returncode, stdout, _ = self.rerun_if_necessary(returncode, stdout, stderr,
+                                                    command, env)
+
+    if returncode == 0:
+      logging.info('gn check reported no issues.')
+      return True
+
+    self.gn_check_to_review(stdout)
+    return False
diff --git a/cq/scripts/slave/step/gn_step.py b/cq/scripts/slave/step/gn_step.py
new file mode 100644
index 0000000..c6d6068
--- /dev/null
+++ b/cq/scripts/slave/step/gn_step.py
@@ -0,0 +1,106 @@
+"""Build steps to run GN commands on eureka internal."""
+
+from __future__ import absolute_import
+import os
+
+from helpers import gn_utils
+from slave.step import shell_step
+
+
+class GnGenStep(shell_step.ShellStep):
+  """Build step class to call gn gen as a recipe step."""
+
+  def __init__(self, build_args_product, build_args, out_dir, cwd, *args,
+               **kwargs):
+    self._build_args_product = build_args_product
+    self._build_args = build_args
+    self._out_dir = out_dir
+    super().__init__(name='gn_gen', cwd=cwd, *args, **kwargs)
+
+  def get_commands(self):
+    """Returns which commands to be run.
+
+    Can be overridden by a subclass.
+
+    Returns:
+      List of ShellCommand objects to execute.
+    """
+    gn_import = (
+        'import("//chromecast/internal/build/args/product/{}.gn")').format(
+            self._build_args_product)
+    build_args = self._build_args + self.build_accelerator.gn_flags
+    args_str = '{} {}'.format(gn_import, ' '.join(build_args))
+    cmd_list = [
+        gn_utils.GN_BINARY, 'gen', self._out_dir, '--root=chromium/src',
+        '--check', '--args={}'.format(args_str)
+    ]
+    return [self.ShellCommand(cmd_list, validator=self._validator)]
+
+
+class GnArgsStep(shell_step.ShellStep):
+  """Build step class to call gn args as a recipe step."""
+
+  def __init__(self, out_dir, cwd, *args, **kwargs):
+    cmd_list = [
+        gn_utils.GN_BINARY,
+        'args',
+        out_dir,
+        '--root=chromium/src',
+        '--list',
+        '--short',
+        '--overrides-only',
+    ]
+    super().__init__(command=cmd_list, name='gn_list', cwd=cwd, *args, **kwargs)
+
+
+class GnGenCompilationDBStep(shell_step.ShellStep):
+  """Build step class to call gn gen as a recipe step."""
+
+  def __init__(self, out_dir, cwd, build_args_product, target_name, *args,
+               **kwargs):
+    self._build_args_product = build_args_product
+    self._target_name = target_name
+    self._out_dir = out_dir
+    self._build_args = [
+        'use_rbe=false', 'use_goma=false', 'clang_use_chrome_plugins=false'
+    ]
+    super().__init__(name='gn_gen_compdb', cwd=cwd, *args, **kwargs)
+
+  def get_compdb_file_location(self):
+    return os.path.join(self._out_dir, 'compile_commands.json')
+
+  def get_commands(self):
+    """Returns which commands to be run.
+
+    Can be overridden by a subclass.
+
+    Returns:
+      List of ShellCommand objects to execute.
+    """
+    commands = []
+
+    # Re-generate the GN arguments for the product in order to disable build
+    # accelerators and clang_use_chrome_plugins. Otherwise, the Kythe analyzer
+    # will err as it does not have context of these compiler directives.
+    gn_import = (
+        'import("//chromecast/internal/build/args/product/{}.gn")').format(
+            self._build_args_product)
+    args_str = '{} {}'.format(gn_import, ' '.join(self._build_args))
+    args_with_acceleration_disabled = '--args={}'.format(args_str)
+
+    # Generate the compdb (compile_commands.json) file.
+    compile_commands_flag = '--export-compile-commands={}'.format(
+        self._target_name)
+
+    gen_compdb_command = [
+        gn_utils.GN_BINARY,
+        'gen',
+        self._out_dir,
+        '--root=chromium/src',
+        args_with_acceleration_disabled,
+        compile_commands_flag,
+    ]
+    commands.append(
+        self.ShellCommand(gen_compdb_command, validator=self._validator))
+
+    return commands
diff --git a/cq/scripts/slave/step/gotham_build_step.py b/cq/scripts/slave/step/gotham_build_step.py
new file mode 100644
index 0000000..dc6fcae
--- /dev/null
+++ b/cq/scripts/slave/step/gotham_build_step.py
@@ -0,0 +1,37 @@
+"""Build step class for building Gotham."""
+
+from __future__ import absolute_import
+from slave import base_step
+from slave.step import shell_step
+
+
+class GothamBuildStep(shell_step.ShellStep):
+  """Step for building Catatester Binaries."""
+
+  def __init__(self, build_name, board, **kwargs):
+    """Creates a GothamBuildStep instance.
+
+    Args:
+      build_name: The type of build.
+      board: Name of the board.
+      **kwargs: Any additional args to pass to ShellStep.
+    """
+    self._build_name = build_name
+    self._board = board
+    shell_step.ShellStep.__init__(self, command=['./build_gotham.sh'], **kwargs)
+
+  def run(self):
+    """Runs ShellStep to build gotham after appending venv path.
+
+    Returns:
+      result of ShellStep
+    """
+
+    self._command.append('--skip_setup_venv')
+    self._command.append('--out_dir={}'.format(self.get_gcs_dir()))
+    self._command.append(
+        '--venv_install_dir={}'.format(self.get_step_data('virtual_env_root')))
+    self._command.append(self.build_number)
+    self._command.append(self._build_name)
+    self._command.append(self._board)
+    return shell_step.ShellStep.run(self)
diff --git a/cq/scripts/slave/step/host_tests_step.py b/cq/scripts/slave/step/host_tests_step.py
new file mode 100644
index 0000000..5c45bcd
--- /dev/null
+++ b/cq/scripts/slave/step/host_tests_step.py
@@ -0,0 +1,34 @@
+"""Run the Android.mk host tests."""
+
+from __future__ import absolute_import
+import os
+
+from slave.step import shell_step
+
+
+_TEST_PROJECT = 'test'
+_RUN_HOST_TEST_SCRIPT = 'unit_test/run_host_tests.py'
+
+
+class RunHostTestsStep(shell_step.ShellStep):
+  """Step to run the Android.mk host tests."""
+
+  def __init__(self, env=None, **kwargs):
+    del env  # Unsused
+    shell_step.ShellStep.__init__(self, name='run host tests', **kwargs)
+
+  def get_commands(self):
+    """Return commands to run the host tests.
+
+    After building the `make test-build-target`, these tests
+    can be found in out/host/*/bin/* and include tests
+    such as usbtest and led_unittests.
+
+    Returns:
+      List of ShellCommand objects to execute to run the host tests.
+    """
+    test_project_dir = self.get_project_path(_TEST_PROJECT)
+    run_test_script = os.path.join(test_project_dir, _RUN_HOST_TEST_SCRIPT)
+    return [self.ShellCommand(['python3', run_test_script],
+                              cwd=os.getcwd(),
+                              env={'PYTHONPATH': test_project_dir})]
diff --git a/cq/scripts/slave/step/include_what_you_use_step.py b/cq/scripts/slave/step/include_what_you_use_step.py
new file mode 100644
index 0000000..84aed50
--- /dev/null
+++ b/cq/scripts/slave/step/include_what_you_use_step.py
@@ -0,0 +1,82 @@
+"""Build step class for analyzing C/C++ include directives"""
+
+# TODO(mingcl): This step is WIP
+
+from collections import defaultdict
+import logging
+import os
+import yaml
+
+from helpers import commit_message_utils
+from helpers import diff_utils
+from helpers import git_utils
+from slave import base_step
+
+GET_GIT_MODIFIED_FILE_NAMES = ['git', 'diff', '--name-only', 'HEAD^']
+
+
+class IncludeWhatYouUseStep(base_step.BaseStep):
+  """Build step class for running iwyu on C/C++ files."""
+
+  def __init__(self, **kwargs):
+    """Creates a IncludeWhatYouUseStep instance.
+
+    Note that this step uses clang-tidy-diff, so that only the changed
+    parts of a file are checked.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='clang-tidy', **kwargs)
+
+  def _filter_files(self, filenames):
+    """Filter out the files that we are going to pass to iwyu tool """
+    return [
+        filename for filename in filenames if filename.endswith('.cc') and
+        self.allow_listed(filename) and not self.deny_listed(filename)
+    ]
+
+  def run(self):
+    """Runs iwyu on changed files for the current commit.
+
+    Returns:
+      True iff there were no suggestions.
+    """
+    # Do not run on cherry-picks; errors are ignored for upstream consistency
+    if git_utils.is_cherry_pick(self, self.directory):
+      logging.info('iwyu check skipped because this is a cherry-pick.')
+      return True
+
+    # Do not run if the build looks like a revert.
+    commit_message = git_utils.commit_message(self, self.directory)
+    if commit_message_utils.is_revert_commit_message(commit_message):
+      logging.info('looks like a revert, allowing it through.')
+      return True
+
+    _, stdout, _ = self.exec_subprocess(
+        GET_GIT_MODIFIED_FILE_NAMES, cwd=self.directory)
+    cc_file_names = self._filter_files(stdout.splitlines())
+
+    if not cc_file_names:
+      logging.info('No files to check.')
+      return True
+
+    # TODO(mingcl): Figure out if there is edge cases that checking .cc files
+    # alone is not enough
+    logging.info('Going to check the following files: %s',
+                 ' '.join(cc_file_names))
+
+    # TODO(mingcl): Don't hard code the path of compilation database as ../../
+    returncode, stdout, _ = self.exec_subprocess(
+        ['iwyu_tool', '-p', '../../'] + cc_file_names, cwd=self.directory)
+
+    if returncode != 0:
+      logging.info('Some errors happened when running iwyu_tool')
+      return False
+
+    # TODO(mingcl): Analysis iwyu output here
+
+    self.add_review({'message': stdout})
+
+    # TODO(mingcl): Return false when there are iwyu violations
+    return True
diff --git a/cq/scripts/slave/step/jacoco_coverage_processing_step.py b/cq/scripts/slave/step/jacoco_coverage_processing_step.py
new file mode 100644
index 0000000..ac4b719
--- /dev/null
+++ b/cq/scripts/slave/step/jacoco_coverage_processing_step.py
@@ -0,0 +1,276 @@
+"""Build step class for processing jacoco exec files."""
+import collections
+import json
+import logging
+import os
+import pathlib
+import zipfile
+import xml.etree.ElementTree as ET
+
+from slave.step import cast_shell_step
+
+COVERAGE_SCRIPT_PATH = 'build/android/generate_jacoco_report.py'
+SOURCES_JSON_SUFFIX = '__jacoco_sources.json'
+MAPPINGS_FILE = 'jcov_file_mappings.json'
+
+class JacocoCoverageProcessingStep(cast_shell_step.NonOtaBuildBaseStep):
+  """Build step class to process jacoco code coverage data."""
+
+  def __init__(
+      self,
+      branding,
+      build_type,
+      product,
+      build_flavor,
+      coverage_dir,
+      report_out_dir='report',
+      report_format='xml',
+      sanitizer=None,
+      build_args_product=None,
+      build_args_official=None,
+      **kwargs,
+  ):
+    """Creates a JacocoCoverageProcessingStep instance.
+
+      Args:
+        branding: Build branding (chromium or chrome).
+        build_type: Type of system to build for ('x86', 'clang', or 'arm').
+        product: product to build for (ie. 'chromecast', 'audio').
+        build_flavor: Flavor of build ('Debug', 'Eng', 'Release').
+        coverage_dir: Directory under the build directory that contains the
+          jacoco exec files.
+        report_out_dir: Directory under the gcs directory, to output coverage
+          report.
+        sanitizer: Clang sanitizer to run with
+          ('asan', 'msan', 'tsan', 'ubsan').
+        build_args_product: The name of one of the
+          chromecast/internal/build/args/product files to use for the GN args.
+        report_format: Output format of the jacoco coverage report
+          ('html', 'xml', 'csv').
+        build_args_official: Whether the build is an official build. MUST be set
+          for any build that will ship to users. Disables debugging; do not
+          set for development builds.
+        **kwargs: Any additional args to pass to BaseStep.
+      """
+    super().__init__(
+        'process jacoco code coverage data.',
+        branding,
+        build_type,
+        product,
+        build_flavor,
+        sanitizer,
+        build_args_product,
+        build_args_official,
+        **kwargs,
+    )
+    self._coverage_dir = coverage_dir
+    self._report_out_dir = report_out_dir
+    self._report_format = report_format
+
+    assert report_format in ['xml', 'html', 'csv']
+
+  def get_output_report_path(self):
+    if self._report_format == 'xml':
+      logging.info('gcs dir:%s', self.get_gcs_dir())
+      path = (
+          pathlib.Path(self.get_gcs_dir())
+          / self._report_out_dir
+          / 'processed_report.xml'
+      )
+      return path
+
+  def _remap_coverage_report_paths(self, input_xml_report, output_xml_report):
+    """Creates a new xml file which has the file path as in the fs.
+
+    Args:
+      input_xml_report: path to xml report.
+      output_xml_report: path to write the procesed xml report.
+    """
+    jacoco_xml = ET.parse(input_xml_report)
+    mapped_files = set()
+
+    for root, _, files in os.walk(self._outdir, followlinks=True):
+      for file in files:
+        if file.endswith(SOURCES_JSON_SUFFIX):
+          file_path = os.path.join(root, file)
+          mapped_files = mapped_files.union(
+              self._get_mappings_from_sources_json(file_path, jacoco_xml)
+          )
+
+    self._update_coverage_report_xml(
+        mapped_files,
+        jacoco_xml,
+        output_xml_report,
+    )
+
+  def _update_coverage_report_xml(
+      self,
+      source_to_workspace_path_mapping,
+      report_contents,
+      output_report_path,
+  ):
+    root = report_contents.getroot()
+
+    new_packages = collections.defaultdict(lambda: ET.Element('package'))
+
+    new_root = ET.Element('report')
+    new_tree = ET.ElementTree(new_root)
+
+    for file_mapping in source_to_workspace_path_mapping:
+      pkg_name, file_name, real_path = file_mapping
+      real_package_path = str(pathlib.Path(real_path).parent)
+      file_element = root.find(
+          f'.//package[@name="{pkg_name}"]//sourcefile[@name="{file_name}"]'
+      )
+      if file_element is None:
+        continue
+
+      new_packages[real_package_path].set('name', real_package_path)
+      new_packages[real_package_path].append(file_element)
+
+    for new_package in new_packages.values():
+      new_root.append(new_package)
+
+    new_tree.write(output_report_path, encoding='utf-8')
+
+  def _get_mappings_from_sources_json(self, sources_json_file, jacoco_xml):
+    """Maps the files to their real paths.
+
+    Args:
+      sources_json_file: path to sources json file.
+      jacoco_xml: object for parsing xml report.
+    """
+    with open(sources_json_file) as file:
+      parsed_json = json.load(file)
+
+    mapped_files = set()
+    src_dirs_sorted = sorted(parsed_json['source_dirs'], key=len)
+
+    for jar_file in parsed_json['input_path']:
+      with zipfile.ZipFile(jar_file, 'r') as zf:
+        for java_class_file in zf.infolist():
+          package, sourcefile = self._get_filepath_from_classpath(
+              java_class_file.filename, jacoco_xml)
+
+          if package and sourcefile:
+            real_path = self._get_path_to_file_from_src_dirs(
+                src_dirs_sorted, package, sourcefile)
+
+            if real_path:
+              mapped_files.add((package, sourcefile, real_path))
+
+    return mapped_files
+
+  def _get_filepath_from_classpath(self, class_file, jacoco_xml):
+    """Given the class file name and the jacoco_xml object, this function
+       returns the package name and the filename of the class.
+
+    Args:
+      class_file: filepath to a class relative to the JAR's root.
+      jacoco_xml: the parsed Jacoco XML report.
+    """
+    class_name = class_file.split('.')[0]
+    package = self._get_package_from_class_file(class_file)
+    root = jacoco_xml.getroot()
+
+    class_element = root.find(f'.//class[@name="{class_name}"]')
+
+    # For some reason, the bool value of some of the element objects are
+    # `False`.
+    if class_element is not None:
+      filename = class_element.attrib['sourcefilename']
+
+      return package, filename
+
+    return None, None
+
+  def _get_package_from_class_file(self, class_file):
+    """Given the class file name, this function returns the package name.
+
+    Args:
+      class_file: name of class file.
+    """
+    return class_file.rsplit('/', 1)[0]
+
+  def _get_path_to_file_from_src_dirs(self, src_dirs, pkg_name, sourcefile):
+    """Returns the path to the file, given the `source_dirs` list from the
+       sources json file, the name of the package, and the sourcefile name.
+
+    Args:
+      src_dirs: `source_dirs` list from sources json file. src_dirs should be
+                sorted by length of package to avoid issues with overlapping
+                package names.
+      pkg_name: name of package.
+      sourcefile: name of source file.
+    """
+    for src_dir in src_dirs:
+      if src_dir.endswith(pkg_name):
+        path = os.path.join('chromium/src', src_dir, sourcefile)
+
+        # Resolve symlinks.
+        path = os.path.join(self._getcwd(), path)
+        path = os.path.realpath(path)
+        path = os.path.relpath(path, self._getcwd())
+
+        return path
+
+    return None
+
+  def run(self):
+    """Runs the generate jacoco report script and generates a coverage report
+    from the jacoco `exec` files.
+
+    Returns:
+      True iff scripts ran successfuly with no errors.
+    """
+    coverage_script_path = os.path.join(
+        self.get_project_path('chromium/src'), COVERAGE_SCRIPT_PATH)
+
+    vpython3_path = os.path.join(
+        self.get_project_path('chromium/tools/depot_tools'),
+        'vpython3',
+    )
+
+    command = [vpython3_path, coverage_script_path]
+    command += ['--format', self._report_format]
+
+    report_path = os.path.join(self.get_gcs_dir(), self._report_out_dir)
+
+    # Make sure directory exists.
+    os.makedirs(report_path, exist_ok=True)
+
+    if self._report_format == 'html':
+      command += ['--output-dir', report_path]
+    else:
+      report_file = os.path.join(report_path, f'report.{self._report_format}')
+
+      command += ['--output-file', report_file]
+
+    coverage_dir_path = os.path.join(self._outdir, self._coverage_dir)
+
+    if (not os.path.exists(coverage_dir_path) or
+        not os.path.isdir(coverage_dir_path) or
+        not os.listdir(coverage_dir_path)):
+      logging.info(
+          'No files found under coverage directory: %s... Not processing '
+          'coverage files',
+          coverage_dir_path,
+      )
+      return True
+
+    command += ['--sources-json-dir', self._outdir]
+    command += ['--coverage-dir', coverage_dir_path]
+    command += ['--device-or-host', 'host']
+
+    returncode, _, _ = self.exec_subprocess(command)
+
+    # Create the mappings file only for xml reports, as this is the only
+    # format that is accepted by the coverage processor at the moment.
+    if self._report_format == 'xml':
+      output_path = self.get_output_report_path()
+      self._remap_coverage_report_paths(report_file, output_path)
+      self.set_build_property(
+          'coverage_jacoco_files',
+          [str(output_path.relative_to(self.get_gcs_dir()))])
+
+    return returncode == 0
diff --git a/cq/scripts/slave/step/json_check_step.py b/cq/scripts/slave/step/json_check_step.py
new file mode 100644
index 0000000..ff6b1e0
--- /dev/null
+++ b/cq/scripts/slave/step/json_check_step.py
@@ -0,0 +1,55 @@
+"""Build step class for validating JSON files."""
+
+from __future__ import absolute_import
+import collections
+import json
+import os
+import re
+
+from helpers import git_utils
+from slave import base_step
+
+_JSON_PARSE_ERROR_MSG = 'This JSON file failed to parse. {}'
+_JSON_ERROR_POSITION_PATTERN = r'line (?P<linenum>\d+) column \d+'
+
+
+class JsonCheckStep(base_step.BaseStep):
+  """Build step class for validating JSON files."""
+
+  def __init__(self, **kwargs):
+    """Creates a JsonCheckStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='json check', **kwargs)
+
+  def _value_error_to_comment(self, err):
+    comment = {'message': _JSON_PARSE_ERROR_MSG.format(str(err))}
+    match = re.search(_JSON_ERROR_POSITION_PATTERN, str(err))
+    if match:
+      comment['line'] = match.group('linenum')
+    return comment
+
+  def run(self):
+    """Attempts to parse all .json files in the current commit.
+
+    Sets a build property "review" with the output if there are any errors.
+
+    Returns:
+      True iff there were no parsing errors.
+    """
+    comments = collections.defaultdict(list)
+    for json_filename in self.changed_files(file_extensions=['.json']):
+      with open(os.path.join(self.directory, json_filename)) as json_file:
+        try:
+          json.load(json_file)
+        except ValueError as err:
+          comments[json_filename].append(self._value_error_to_comment(err))
+
+    if comments:
+      self.add_review({'comments': comments})
+      return False
+
+    # No Errors
+    return True
diff --git a/cq/scripts/slave/step/kythe_step.py b/cq/scripts/slave/step/kythe_step.py
new file mode 100644
index 0000000..d37d48f
--- /dev/null
+++ b/cq/scripts/slave/step/kythe_step.py
@@ -0,0 +1,76 @@
+"""Build steps to run Kythe commands on eureka internal."""
+
+import os
+
+from helpers import git_utils
+from slave.step import shell_step
+
+KYTHE_RUN_EXTRACTOR_BINARY = \
+  "chromecast/internal/third_party/kythe/tools/runextractor"
+KYTHE_CXX_EXTRACTOR_BINARY = \
+  "chromecast/internal/third_party/kythe/extractors/cxx_extractor"
+KYTHE_KZIP_BINARY = "chromecast/internal/third_party/kythe/tools/kzip"
+EUREKA_INTERNAL_CORUPUS = "eureka-internal.googlesource.com/codesearch"
+VNAMES_JSON_FILE_LOCATION = \
+  "cq/scripts/resources/kythe_step/vnames.json"
+
+
+class GenerateKzipStep(shell_step.ShellStep):
+
+  def __init__(self, cwd, tests_root, chromium_root, out_dir, compdb_file,
+               min_expected_kzip_bytes, *args, **kwargs):
+    super().__init__(name="kythe_gen", cwd=cwd, *args, **kwargs)
+
+    self._tests_root = tests_root
+    self._chromium_root = chromium_root
+    self._compdb_file = compdb_file
+    self._min_kzip_size = min_expected_kzip_bytes
+
+    self._runextractor_binary = os.path.join(self._chromium_root,
+                                             KYTHE_RUN_EXTRACTOR_BINARY)
+    self._cxx_extractor_binary = os.path.join(self._chromium_root,
+                                              KYTHE_CXX_EXTRACTOR_BINARY)
+    self._kzip_binary = os.path.join(self._chromium_root, KYTHE_KZIP_BINARY)
+    self._kythe_output_directory = os.path.join(out_dir, "kythe-output")
+    head_sha = git_utils.get_head_sha(self, directory=self._chromium_root)
+    if head_sha is None:
+      raise ValueError("Head SHA could not be determined for {}".format(
+          self._chromium_root))
+    self._merged_kzip_file = os.path.join(out_dir, head_sha + ".kzip")
+    self._initialize_env()
+
+  def get_kythe_output_file(self):
+    return self._merged_kzip_file
+
+  def _initialize_env(self):
+    os.environ["KYTHE_ROOT_DIRECTORY"] = os.path.abspath(self._chromium_root)
+    os.environ["KYTHE_CORPUS"] = EUREKA_INTERNAL_CORUPUS
+    os.environ["KYTHE_OUTPUT_DIRECTORY"] = self._kythe_output_directory
+    os.environ["KYTHE_VNAMES"] = os.path.join(self._tests_root,
+                                              VNAMES_JSON_FILE_LOCATION)
+
+  def get_commands(self):
+    cmd_prep_output = ["mkdir", "-p", self._kythe_output_directory]
+    cmd_gen_kzips = [
+        self._runextractor_binary, "compdb", "-extractor",
+        self._cxx_extractor_binary, "-path", self._compdb_file
+    ]
+    cmd_merge_kzips = [
+        self._kzip_binary, "merge", "--output", self._merged_kzip_file,
+        "--recursive", self._kythe_output_directory
+    ]
+    cmd_validate_kzip = ["du", "-b", self._merged_kzip_file]
+    return [
+        self.ShellCommand(cmd_prep_output, validator=self._validator),
+        self.ShellCommand(cmd_gen_kzips, validator=self._validator),
+        self.ShellCommand(cmd_merge_kzips, validator=self._validator),
+        self.ShellCommand(
+            cmd_validate_kzip, validator=self._validate_merged_kzip)
+    ]
+
+  def _validate_merged_kzip(self, returncode, stdout, stderr):
+    if returncode != 0:
+      return False
+
+    byte_size = stdout.split()[0]
+    return byte_size.isdigit() and int(byte_size) > self._min_kzip_size
diff --git a/cq/scripts/slave/step/landmine_step.py b/cq/scripts/slave/step/landmine_step.py
new file mode 100644
index 0000000..3022f4d
--- /dev/null
+++ b/cq/scripts/slave/step/landmine_step.py
@@ -0,0 +1,87 @@
+"""Landmine step for clobbering output directories when needed.
+
+This step checks for a landmine file in continuous-tests/cq/landmines, and
+clobbers all the output directories if it finds a new landmine.
+
+For more information on using landmines, checkout out the documentation at
+continous-tests/cq/landmines/README.md
+"""
+
+from __future__ import absolute_import
+import logging
+import os
+import re
+
+from helpers import git_utils
+from slave.step import shell_step
+
+LANDMINE_PATTERN = r'landmine\.(?P<landmine_number>\d+)'
+
+
+class LandmineStep(shell_step.ShellStep):
+  """Landmine step for clobbering output directories when needed."""
+
+  def __init__(self, eureka_root, **kwargs):
+    """Creates a LandmineStep instance."""
+    shell_step.ShellStep.__init__(self,
+                                  name='landmine',
+                                  halt_on_failure=True,
+                                  **kwargs)
+    self._landmine_path = eureka_root / '.landmines'
+
+  def get_landmine_dir(self):
+    """Return the directory where new landmine files get checked in."""
+    continuous_tests = self.get_project_path('continuous-tests')
+    return os.path.join(continuous_tests, 'cq', 'landmines')
+
+  def get_landmine(self):
+    """Returns the name of landmine file with the highest number (or None)."""
+    landmine = None
+    max_landmine_number = float('-inf')
+
+    landmine_dir = self.get_landmine_dir()
+    if not os.path.isdir(landmine_dir):
+      return landmine
+
+    for filename in os.listdir(landmine_dir):
+      match = re.search(LANDMINE_PATTERN, filename)
+      if match:
+        logging.info('Found landmine file: %s', filename)
+        landmine_number = int(match.group('landmine_number'))
+        if landmine_number > max_landmine_number:
+          landmine, max_landmine_number = filename, landmine_number
+    logging.info('Highest landmine: %s', landmine)
+    return landmine
+
+  def should_clobber(self, landmine):
+    """Returns True if |landmine| is new, indicating a clobber should happen."""
+    return not (self._landmine_path / landmine).exists()
+
+  def should_mark_landmine_used(self, landmine):
+    """A landmine should only be marked used once it is committed.
+
+    If a landmine is in the patches running through CQ, then do not mark
+    the landmine as used, so that if a builder runs another CL between running
+    the landmine CL and the landmine actually landing, the landmine can run
+    again.
+    """
+    all_patches = [{'project': self.patch_project}] + self.depends_on_list
+    changed_files = git_utils.get_changed_files_in_all_patches(
+        self, all_patches, self.get_project_path_lookup_table())
+    return all(landmine not in f for f in changed_files)
+
+  def get_commands(self):
+    commands = []
+    landmine = self.get_landmine()
+    if landmine and self.should_clobber(landmine):
+      commands.append(self.ShellCommand(['make', 'clean'], cwd=os.getcwd()))
+      commands.append(self.ShellCommand(
+          ['mkdir', '-p', str(self._landmine_path)],
+          cwd=os.getcwd()))
+
+      if self.should_mark_landmine_used(landmine):
+        commands.append(self.ShellCommand(
+            ['touch', str(self._landmine_path / landmine)],
+            cwd=os.getcwd()))
+
+    return commands
diff --git a/cq/scripts/slave/step/libassistant_sdk_step.py b/cq/scripts/slave/step/libassistant_sdk_step.py
new file mode 100644
index 0000000..7c5c2c9
--- /dev/null
+++ b/cq/scripts/slave/step/libassistant_sdk_step.py
@@ -0,0 +1,26 @@
+"""Step class for libassistant sdk validation."""
+
+from __future__ import absolute_import
+import os
+
+from slave import base_step
+
+
+class SdkValidationStep(base_step.BaseStep):
+  """Validate libassistant sdk."""
+
+  def __init__(self, **kwargs):
+    super(SdkValidationStep, self).__init__(name='verify_sdk', **kwargs)
+
+  def run(self):
+    """Run the script to validate sdk code.
+
+    Returns:
+      True iff there were no errors.
+    """
+    verify_script = os.path.join(
+        self.get_project_path('standalone/src'),
+        'build/libassistant/verify_sdk.py')
+
+    returncode, _, _ = self.exec_subprocess([verify_script])
+    return returncode == 0
diff --git a/cq/scripts/slave/step/license_manager_step.py b/cq/scripts/slave/step/license_manager_step.py
new file mode 100644
index 0000000..6a55acd
--- /dev/null
+++ b/cq/scripts/slave/step/license_manager_step.py
@@ -0,0 +1,85 @@
+"""Build step class for setting up a build licenses."""
+
+from __future__ import absolute_import
+import logging
+import os
+import shlex
+
+from slave import base_step
+from slave.step import shell_step
+
+
+_ANDROID_SDK_PATH = [
+    'chromium/src/third_party/android_sdk/public',
+    'chromium/src/third_party/android_tools/sdk',
+]
+_ANDROID_SDK_MANAGER_BIN = 'tools/bin/sdkmanager'
+
+_ANDROID_SDK_MANAGER = 'bash -c "yes {param} | {sdk_manager_bin} --licenses"'
+_ONLY_CHECK_LICENSE = 'n'
+_AUTO_ACCEPT_LICENSE = 'y'
+
+_ANDROID_SDK_LICENSE_OK = 'All SDK package licenses accepted'
+_ANDROID_SDK_LICENSE_FAIL = 'SDK package license not accepted'
+_ANDROID_SDK_LICENSES_FAIL = 'SDK package licenses not accepted'
+
+
+def _get_sdk_manager_command(param):
+  return shlex.split(_ANDROID_SDK_MANAGER.format(
+      param=param, sdk_manager_bin=_ANDROID_SDK_MANAGER_BIN))
+
+
+class LicenseError(base_step.StepError):
+  """Exception raised when an unrecognized license status received."""
+  pass
+
+
+class AndroidSdkLicenseCheckAccept(shell_step.ShellStep):
+  """Build step for checking and accepting Android SDK license."""
+
+  def __init__(self, **kwargs):
+    shell_step.ShellStep.__init__(
+        self,
+        name='Android SDK License Installation',
+        halt_on_failure=True,
+        **kwargs)
+    self.command = None
+
+  def _android_sdk_status_validator(self, returncode, stdout, stderr):
+    del returncode, stderr  # Unused.
+    if _ANDROID_SDK_LICENSE_OK in stdout:
+      logging.info('Android SDK licenses accepted')
+      return True
+    if (_ANDROID_SDK_LICENSE_FAIL in stdout
+        or _ANDROID_SDK_LICENSES_FAIL in stdout):
+      logging.info('Some Android SDK licenses not accepted')
+      return False
+    raise LicenseError('Fail to check status of Android SDK package')
+
+  def _prepare_license_accept(self, returncode, stdout, stderr, cwd=''):
+    """A function to run before retrying a failed repo sync."""
+    del returncode, stdout, stderr  # Unused.
+    logging.info('Auto-accepting Android SDK licenses')
+    self.exec_subprocess(
+        _get_sdk_manager_command(_AUTO_ACCEPT_LICENSE),
+        cwd=cwd)
+
+  def _valid_sdk_path(self):
+    return [sdk_path for sdk_path in
+            _ANDROID_SDK_PATH if os.path.isfile(os.path.join(
+                sdk_path, _ANDROID_SDK_MANAGER_BIN))]
+
+  def get_commands(self):
+    return [
+        self.ShellCommand(
+            _get_sdk_manager_command(_ONLY_CHECK_LICENSE),
+            cwd=sdk_path,
+            num_retries=1,
+            validator=self._android_sdk_status_validator,
+            setup_for_retry=(
+                lambda returncode, stdout, stderr, cwd=sdk_path:
+                self._prepare_license_accept(
+                    returncode, stdout, stderr, cwd=cwd)),
+            halt_on_failure=True)
+        for sdk_path in self._valid_sdk_path()
+    ]
diff --git a/cq/scripts/slave/step/luci_recipe_step.py b/cq/scripts/slave/step/luci_recipe_step.py
new file mode 100644
index 0000000..6e33bcf
--- /dev/null
+++ b/cq/scripts/slave/step/luci_recipe_step.py
@@ -0,0 +1,68 @@
+"""Executes LUCI recipe tests."""
+from __future__ import absolute_import
+import os
+
+from helpers import luci_recipe_utils
+from slave import base_step
+
+_STEP_TIMEOUT_SECS = 5 * 60  # 5 minutes.
+
+
+class LUCIRecipeBaseStep(base_step.BaseStep):
+  """Step for executing LUCI recipe commands for a single recipe package."""
+
+  def __init__(self,
+               script_path,
+               package_path,
+               timeout_secs=_STEP_TIMEOUT_SECS,
+               **kwargs):
+    super(LUCIRecipeBaseStep, self).__init__(
+        timeout_secs=timeout_secs, **kwargs)
+    script_abspath = os.path.join(self.directory, script_path)
+    package_abspath = os.path.join(self.directory, package_path)
+
+    self._recipe_runner = luci_recipe_utils.LUCIRecipeRunner(
+        self, script_abspath, package_path=package_abspath)
+
+
+class LUCIRecipeTestStep(LUCIRecipeBaseStep):
+  """Step for executing LUCI recipe expectation tests."""
+
+  def __init__(self,
+               script_path,
+               package_path,
+               test_retries,
+               **kwargs):
+    super().__init__(
+        script_path, package_path, **kwargs)
+    self._test_retries = test_retries
+
+  def run(self):
+    retries = 0
+    while retries <= self._test_retries:
+      returncode, _, _ = self._recipe_runner.run_tests()
+      if returncode == 0:
+        return True
+      retries += 1
+
+    # Max retries reached.
+    self.add_review({'message': 'Recipe tests failed! See step logs.'})
+    return False
+
+
+class LUCIRecipeBundleStep(LUCIRecipeBaseStep):
+  """Step for bundling a LUCI recipe."""
+
+  def __init__(self, script_path, package_path, destination, **kwargs):
+    super(LUCIRecipeBundleStep, self).__init__(
+        script_path, package_path, **kwargs)
+    self._destination_abspath = os.path.join(self.directory, destination)
+
+  def run(self):
+    returncode, _, _ = self._recipe_runner.bundle(self._destination_abspath)
+
+    if returncode != 0:
+      self.add_review({
+          'message': 'Failed to generate recipe bundle! See step logs.'})
+      return False
+    return True
diff --git a/cq/scripts/slave/step/lucicfg_step.py b/cq/scripts/slave/step/lucicfg_step.py
new file mode 100644
index 0000000..6244be6
--- /dev/null
+++ b/cq/scripts/slave/step/lucicfg_step.py
@@ -0,0 +1,66 @@
+"""Steps for executing lucicfg commands."""
+
+from __future__ import absolute_import
+import logging
+import os
+
+from slave import base_step
+from helpers import os_utils
+
+_CONFIG_PATHS_KEY = 'lucicfg_paths'
+
+
+class FindConfigsStep(base_step.BaseStep):
+  """Step for finding all `main.star` luci configs."""
+
+  def __init__(self, **kwargs):
+    super(FindConfigsStep, self).__init__(name='Find luci configs', **kwargs)
+
+  def _get_config_paths(self):
+    return os_utils.find_files(self.directory, 'main.star')
+
+  def run(self):
+    config_paths = self._get_config_paths()
+    logging.info(config_paths)
+    self.add_step_data(_CONFIG_PATHS_KEY, config_paths)
+    return True
+
+
+class LucicfgValidateStep(base_step.BaseStep):
+  """Step for running `lucicfg validate` on `main.star` files."""
+
+  def __init__(self, **kwargs):
+    super(LucicfgValidateStep, self).__init__(name='Validate configs', **kwargs)
+
+  def _validate_config(self, config_path):
+    """Runs `lucicfg validate` against a single config."""
+    cmd = ['lucicfg', 'validate', config_path]
+
+    # Validate with GCE service account, if specified.
+    if self.get_property('gce_service_account'):
+      cmd.extend(['-service-account-json',
+                  self.get_property('gce_service_account')])
+
+    retcode, _, stderr = self.exec_subprocess(cmd)
+
+    if retcode == 0:
+      logging.info('%s is valid!', config_path)
+      return True
+
+    self.add_review({'message': stderr})
+    logging.error('%s is NOT valid!', config_path)
+    return False
+
+  def run(self):
+    """Runs the lucicfg validate step.
+
+    Attempts to validate every luci config found in the directory.
+    Expects depot_tools to be on $PATH.
+    """
+    config_paths = self.get_step_data(_CONFIG_PATHS_KEY)
+    results = []
+    for config_path in config_paths:
+      result = self._validate_config(config_path)
+      results.append(result)
+
+    return all(results)
diff --git a/cq/scripts/slave/step/make_clean_step.py b/cq/scripts/slave/step/make_clean_step.py
new file mode 100644
index 0000000..b71d813
--- /dev/null
+++ b/cq/scripts/slave/step/make_clean_step.py
@@ -0,0 +1,62 @@
+"""Make clean step."""
+
+from __future__ import absolute_import
+import logging
+
+from helpers import branch_utils
+from slave import base_step
+
+
+class MakeCleanStep(base_step.BaseStep):
+  """Make clean step class."""
+
+  def __init__(self, clean_dir=None, **kwargs):
+    """Creates a MakeCleanStep instance.
+
+    Args:
+      clean_dir: Dir where to run make clean
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    self._clean_dir = clean_dir
+    base_step.BaseStep.__init__(self, name='clean', **kwargs)
+
+  def run(self):
+    # Do all the cleaning.
+    command = ['make', 'clean']
+    returncode, _, _ = self.exec_subprocess(command, cwd=self._clean_dir)
+
+    return returncode == 0
+
+
+class MakeDistCleanStep(base_step.BaseStep):
+  """Make clean step class."""
+
+  def __init__(self, **kwargs):
+    """Creates a MakeCleanStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='distclean', **kwargs)
+    self._clean_project = self.get_property('make_distclean_project')
+    self._min_branch = self.get_property('make_distclean_min_branch')
+
+  def run(self):
+    # Do all the cleaning.
+    command = ['make', 'distclean']
+
+    if not branch_utils.is_branch_equal_to_or_later_than(
+        self.manifest_branch, self._min_branch):
+      logging.info('Branch is older than %s, nothing to do.', self._min_branch)
+      return True
+
+    if self._clean_project:
+      clean_dir = self.get_project_path(self._clean_project)
+    else:
+      clean_dir = None
+    returncode, _, _ = self.exec_subprocess(
+      command,
+      cwd=clean_dir,
+      env={"VERBOSE": "1"})
+
+    return returncode == 0
diff --git a/cq/scripts/slave/step/ninja_step.py b/cq/scripts/slave/step/ninja_step.py
new file mode 100644
index 0000000..650c9b9
--- /dev/null
+++ b/cq/scripts/slave/step/ninja_step.py
@@ -0,0 +1,25 @@
+"""Build step class for executing ninja commands."""
+
+from __future__ import absolute_import
+import os
+
+from slave.step import shell_step
+
+
+class NinjaStep(shell_step.ShellStep):
+  """Build step class to call ninja commands as recipe steps."""
+
+  def __init__(self, out_dir, target, cwd, jobs,  *args, **kwargs):
+    cmd_list = [
+        'ninja',
+        '-C',
+        out_dir,
+        '-k',
+        '500',
+        '-j',
+        str(jobs),
+        target
+    ]
+    name = 'ninja {}'.format(target)
+    super(NinjaStep, self).__init__(
+        command=cmd_list, name=name, cwd=cwd, *args, **kwargs)
diff --git a/cq/scripts/slave/step/ota_build_tests_step.py b/cq/scripts/slave/step/ota_build_tests_step.py
new file mode 100644
index 0000000..2895c1b
--- /dev/null
+++ b/cq/scripts/slave/step/ota_build_tests_step.py
@@ -0,0 +1,50 @@
+"""Build step class for building OTA tests.."""
+
+from __future__ import absolute_import
+from slave import base_step
+from slave.step import ota_step
+
+
+class OtaBuildTestsStep(ota_step.OtaStep):
+  """Build step class for building OTA tests."""
+
+  def __init__(self, build_name, board_name=None, **kwargs):
+    """Creates a OtaBuildTestsStep instance.
+
+    Args:
+      build_name: Name of build (e.g. anchovy-eng, tvdefault-eng)
+      board_name: optional BOARD_NAME param for make if any.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    ota_step.OtaStep.__init__(self, build_name=build_name,
+                              name='build ota tests',
+                              board_name=board_name, **kwargs)
+
+  def run(self):
+    """Builds ota tests.
+
+    Returns:
+      True iff there were no errors.
+    """
+    # Because the out/host directory is shared, the host tests can fail if
+    # the intermediate artifacts are not cleared and rebuilt correctly. Force
+    # all of out/hosts to get rebuilt everytime by clearing out/host entirely.
+    command = ['rm', '-rf', 'out/host']
+    self.exec_subprocess(command)
+
+    command = ['make',
+               '-j{}'.format(self.get_num_jobs(
+                   multiplier=ota_step.BUILD_ACCELERATION_JOB_MULTIPLIER)),
+               'PRODUCT-' + self._build_name,
+               'tests-build-target',
+               'DISABLE_AUTO_INSTALLCLEAN=true']
+    command += self.build_accelerator.make_flags
+    command += self._make_extra_flags()
+
+    returncode, stdout, stderr = self.exec_subprocess(
+        command, env=self.make_command_env())
+
+    if returncode != 0:
+      self._error_processor(stdout, stderr)
+
+    return returncode == 0
diff --git a/cq/scripts/slave/step/ota_factory_tool_step.py b/cq/scripts/slave/step/ota_factory_tool_step.py
new file mode 100644
index 0000000..3f116ca
--- /dev/null
+++ b/cq/scripts/slave/step/ota_factory_tool_step.py
@@ -0,0 +1,98 @@
+"""Compile Factory Tools"""
+
+from __future__ import absolute_import
+import os
+import shutil
+
+from slave import base_step
+from slave.step import ota_step
+
+
+class CompileFactoryToolStep(ota_step.OtaStep):
+  """Resign step class for building Factory Tools"""
+
+  def __init__(self, build_name, factory_config,
+               name='build factory tools', **kwargs):
+    """Creates a CompileFactoryToolStep instance.
+
+    Args:
+      build_name: Name of build (e.g. prince-eng, nq-eng)
+      command: make factory tools command
+      factory_config: factory tool build configs,types
+      name: user-visible name of this step.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    self.factory_config = factory_config
+    ota_step.OtaStep.__init__(self, build_name=build_name, name=name, **kwargs)
+
+  def _make_factory_tools(self):
+    """Builds Factory tools for Products.
+
+    Returns:
+      True if the tool was built successfully, False otherwise.
+    """
+    factory_tool_types = self.factory_config.get('factory_tool_types')
+    if not factory_tool_types:
+      return True
+
+    command = ['make']
+    command += [str(t) for t in factory_tool_types]
+    command += ['BUILD_NUMBER={}'.format(self.build_number)]
+    command += self.make_target_param(True)
+
+    returncode, stdout, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      self._error_processor(stdout, stderr)
+      return False
+
+    if not self._create_factory_dir():
+      return False
+
+    return self._copy_factory_tool_to_factory(factory_tool_types)
+
+  def _copy_factory_tool_to_factory(self, factory_tool_types):
+    """Copy factory tools to tool_type folder"""
+    product = self._build_name.split('-')[0]
+    for factory_tool_type in factory_tool_types:
+      # Use BUILD_NUMBER as factory tools version
+      target_files_zip = self.factory_config['source_tool_path'].format(
+          product=product, tool_type=factory_tool_type,
+          version=self.build_number, ext='zip')
+      target_files_no_ext, zip_ext = os.path.splitext(
+        os.path.basename(target_files_zip))
+      dest_file_path = os.path.join(self._ota_archive_output_dir(), 'factory',
+                                  '{}{}'.format(target_files_no_ext,zip_ext))
+      returncode, _, _ = self.exec_subprocess(
+          ['cp', target_files_zip, dest_file_path])
+      if returncode != 0:
+        return False
+      # Copy factory-runin2_package-<product>-<version>.sh for runin2_package
+      if factory_tool_type == 'runin2_package':
+        runin_sh = self.factory_config['source_tool_path'].format(
+          product=product, tool_type=factory_tool_type,
+          version=self.build_number,ext='sh')
+        runin_sh_dest_path = os.path.join(self._ota_archive_output_dir(),
+                    'factory', '{}{}'.format(target_files_no_ext, '.sh'))
+        returncode, _, _ = self.exec_subprocess(
+          ['cp', runin_sh, runin_sh_dest_path])
+        if returncode != 0:
+          return False
+        # Copy runin profiles and rename with self.BUILD_NUMBER
+        runin_profile_path = self.factory_config['runin_profile_path'].format(
+          product=product)
+        returncode, _, _ = self.exec_subprocess(['cp', '-r', runin_profile_path,
+                            os.path.join(self._ota_archive_output_dir(),
+                                      'factory')])
+        if returncode != 0:
+          return False
+
+    return True
+
+  def _create_factory_dir(self):
+    command = ['mkdir', '-p',
+               os.path.join(self._ota_archive_output_dir(), 'factory')]
+    returncode, _, _ = self.exec_subprocess(command)
+    return returncode == 0
+
+  def run(self):
+    return self._make_factory_tools()
diff --git a/cq/scripts/slave/step/ota_fct_step.py b/cq/scripts/slave/step/ota_fct_step.py
new file mode 100644
index 0000000..808d57a
--- /dev/null
+++ b/cq/scripts/slave/step/ota_fct_step.py
@@ -0,0 +1,82 @@
+"""FCT steps for building OTA's."""
+
+from __future__ import absolute_import
+import os
+import shutil
+
+from slave import base_step
+from slave.step import ota_step
+
+
+class OtaFctCompileStep(ota_step.OtaStep):
+  """FCT Compile step class for building OTA's."""
+
+  def __init__(self, build_name, factory_config,
+               name='build factory images', **kwargs):
+    """Creates a OtaFctCompileStep instance.
+
+    Args:
+      build_name: Name of build (e.g. anchovy-eng, tvdefault-eng)
+      factory_config: hostboot or usbstick config. Each element of the
+        'factory_image_types' will be suffixed with 'image' to form the final
+        make target. For example, the list ['bft', 'sft'] will result in this
+        make command: 'make bftimage sftimage'
+      name: user-visible name of this step.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    self.factory_config = factory_config
+    ota_step.OtaStep.__init__(self, build_name=build_name, name=name, **kwargs)
+
+  def _make_fct(self):
+    """Builds |build_name| FCT packages for the |issue| and |patchset|.
+
+    Returns:
+      True if the OTA package was built successfully, False otherwise.
+    """
+    factory_image_types = self.factory_config.get('factory_image_types')
+    if not factory_image_types:
+      return True
+
+    command = ['make']
+    command += ['{}image'.format(t) for t in factory_image_types]
+    command += ['BUILD_NUMBER={}'.format(self.build_number)]
+    command += self.make_target_param(True)
+
+    returncode, stdout, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      self._error_processor(stdout, stderr)
+      return False
+
+    if not self._create_factory_dir():
+      return False
+
+    return self._copy_factory_target_files_to_out_artifacts(factory_image_types)
+
+  def _copy_factory_target_files_to_out_artifacts(self, factory_image_types):
+    """These are used for resigning factory images e.g. prod reflashimage."""
+    product = self._build_name.split('-')[0]
+    for image_type in factory_image_types:
+      target_files_zip = self.factory_config['source_binary_path'].format(
+          product=product, image_type=image_type)
+      target_files_no_ext, zip_ext = os.path.splitext(
+        os.path.basename(target_files_zip))
+      dest_file_path = os.path.join(self._ota_archive_output_dir(), 'factory',
+                                    '{}-{}-{}-{}{}'.format(product, image_type,
+                                                           target_files_no_ext,
+                                                           self.build_number,
+                                                           zip_ext))
+      returncode, _, _ = self.exec_subprocess(
+          ['cp', target_files_zip, dest_file_path])
+      if returncode != 0:
+        return False
+
+    return True
+
+  def _create_factory_dir(self):
+    command = ['mkdir', '-p',
+               os.path.join(self._ota_archive_output_dir(), 'factory')]
+    returncode, _, _ = self.exec_subprocess(command)
+    return returncode == 0
+
+  def run(self):
+    return self._make_fct()
diff --git a/cq/scripts/slave/step/ota_oss_step.py b/cq/scripts/slave/step/ota_oss_step.py
new file mode 100644
index 0000000..f61e799
--- /dev/null
+++ b/cq/scripts/slave/step/ota_oss_step.py
@@ -0,0 +1,89 @@
+"""OSS step class for building OTA's."""
+
+from __future__ import absolute_import
+import os
+from slave.step import ota_step
+import xml.etree.ElementTree as et
+
+from helpers import repo_utils
+
+MANIFEST_OSS_XML = 'manifest_oss.xml'
+
+
+class OtaOssStep(ota_step.OtaStep):
+  """OSS creation step class for building OTA's."""
+
+  def __init__(self,
+               build_name,
+               name='populate oss manifest and tarball open source files',
+               **kwargs):
+    """Creates a OtaOssStep instance.
+
+    Args:
+      build_name: Name of build (e.g. anchovy-eng, tvdefault-eng)
+      name: user-visible name of this step.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    ota_step.OtaStep.__init__(self, build_name=build_name, name=name, **kwargs)
+
+  def _make_oss(self):
+    """Builds |build_name| OTA package for the |issue| and |patchset|.
+
+    Returns:
+      True if the OTA package was built successfully, False otherwise.
+    """
+    command = ['make',
+               'release_oss',
+               'OSS_ROOT=./{}'.format(self._dist_dir()),]
+    command += self.make_target_param(True)
+
+    returncode, stdout, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      self._error_processor(stdout, stderr)
+
+    return returncode == 0
+
+  def _compile_oss_manifest_and_save(self):
+    """Generates a manifest of oss projects for |product|."""
+    product = self._build_name.split('-')[0]
+    oss_projects_file = os.path.join('vendor/eureka/tools/open_source/',
+                                     product + '_oss_project.txt')
+    repo_manifest_content = repo_utils.get_manifest(self._executor)
+
+    if repo_manifest_content and os.path.isfile(oss_projects_file):
+      root = et.fromstring(repo_manifest_content)
+      with open(oss_projects_file, 'r') as f:
+        oss_project_list = f.read().splitlines()
+        for project in root.findall('project'):
+          if project.attrib['name'] not in oss_project_list:
+            root.remove(project)
+      oss_manifest_content = et.tostring(root, encoding='unicode')
+      with open(MANIFEST_OSS_XML, 'w') as f:
+        f.write(oss_manifest_content)
+      output_dir = self.get_gcs_dir()
+      self.exec_subprocess(['cp', MANIFEST_OSS_XML, output_dir])
+
+  def _clear_oss_artifacts(self):
+    """Removes all OTA artifacts leftover by previous builds."""
+    ota_files = self.glob_files('out/dist/*/chromecast_oss.tgz')
+    if ota_files:
+      self.exec_subprocess(['rm', '-f'] + ota_files)
+
+  def _archive_artifacts(self):
+    """Copies the artifacts for archival."""
+    output_dir = self.get_gcs_dir()
+
+    if self.build_system == 'catabuilder':
+      dist_files = self.glob_files(
+          '{}/chromecast_oss.tgz'.format(self._dist_dir()))
+      for f in dist_files:
+        self.exec_subprocess(['cp', '-r', f, output_dir])
+
+  def run(self):
+    self._clear_oss_artifacts()
+    self._compile_oss_manifest_and_save()
+    if not self._make_oss():
+      return False
+    self._archive_artifacts()
+
+    return True
diff --git a/cq/scripts/slave/step/ota_resign_step.py b/cq/scripts/slave/step/ota_resign_step.py
new file mode 100644
index 0000000..16e0983
--- /dev/null
+++ b/cq/scripts/slave/step/ota_resign_step.py
@@ -0,0 +1,1199 @@
+"""Resign step class for building OTA's."""
+
+from __future__ import absolute_import
+from helpers import branch_utils
+from slave import base_step
+from slave.step import ota_step
+
+SOURCE_BINARY_PATH = ('out/target/product/{product}/obj/PACKAGING/'
+                      'target_files_intermediates/'
+                      '{product}-target_files-{buildset}.zip')
+
+SOURCE_FACTORY_PATH = ('out/target/product/{product}/factory/'
+                       '{image_type}/hostboot-target-files.zip')
+
+SOURCE_USBSTICK_PATH = ('out/target/product/{product}/factory/'
+                        '{image_type}/usbstick-target-files.zip')
+
+DESTINATION_USBSTICK_IMAGE_PATH = ('{output_dir}/factory/{product}-'
+                                   '{image_type}-{variant}-{buildset}.img')
+
+SOURCE_FACTORY_TOOL_PATH = ('out/target/product/{product}/'
+                            'factory-{tool_type}-{product}-{version}.{ext}')
+
+RUNIN2_PROFILE_PATH = ('out/target/product/{product}/'
+                        'factory/profile/')
+
+FCT_ZIP_PATH = ('out/target/product/{product}/factory/'
+                '{product}-fct-{buildset}.zip')
+
+AMLOGIC_RESIGN_COMMAND = [
+    './vendor/amlogic/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+BREZZA_RESIGN_COMMAND = [
+    './vendor/qualcomm/brezza/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+ELAINE_RESIGN_COMMAND = [
+    './vendor/amlogic/elaine/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+
+ESTELLE_RESIGN_COMMAND = [
+    './vendor/amlogic/estelle/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+KORLAN_RESIGN_COMMAND = [
+    './vendor/amlogic/korlan/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+LUMIA_RESIGN_COMMAND = [
+    './vendor/amlogic/lumia/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+GQ_RESIGN_COMMAND = [
+    './vendor/amlogic/gq/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+NEWMAN_RESIGN_COMMAND = [
+    './vendor/amlogic/newman/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+NQ_RESIGN_COMMAND = [
+    './vendor/amlogic/nq/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+SIROCCO_RESIGN_COMMAND = [
+    './vendor/qualcomm/sirocco/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+SPENCER_RESIGN_COMMAND = [
+    './vendor/amlogic/spencer/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+A049_RESIGN_COMMAND = [
+    './vendor/amlogic/a049/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+SPRINKLES_RESIGN_COMMAND = [
+    './vendor/amlogic/korlan/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+TQ_RESIGN_COMMAND = [
+    './vendor/amlogic/tq/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+MARVELL_RESIGN_COMMAND = [
+    './vendor/marvell/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+VALENS_UNAND_COMMAND = [
+    './vendor/marvell/valens/build/tools/mk-fct-unand.sh'
+]
+
+VENTO_UNAND_COMMAND = [
+    './vendor/marvell/vento/build/tools/mk-fct-unand.sh'
+]
+
+VENUS_RESIGN_COMMAND = [
+    './vendor/amlogic/venus/build/tools/releasetools/ota_from_target_files',
+    '-v',
+]
+
+ASSISTANTDEFAULT_RESIGN = {
+    'command': ['cp'],
+    'source_binary_path': '{dist_dir}/{product}-ota-{buildset}.zip',
+    'include_params': False,
+    'variants': [{'board': 'assistantdefault-b1'}],
+}
+
+BIGGIE_RESIGN = {
+    'command': AMLOGIC_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'biggie-b1'},
+        {'board': 'biggie-b1', 'omaha_channel': 'beta-channel'},
+        {'board': 'biggie-b3'},
+        {'board': 'biggie-b3', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+BIGGIE_FACTORY = {
+    'command': AMLOGIC_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'biggie-b1', 'usb_host_package': None},
+        {'board': 'biggie-b3', 'usb_host_package': None},
+        {'board': 'biggie-b4', 'usb_host_package': None},
+    ],
+}
+
+BOWIE_RESIGN = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'bowie-p0'},
+        {'board': 'bowie-p0', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+BOWIE_FACTORY = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'bowie-p0', 'usb_host_package': None},
+    ],
+}
+
+BOWIE_USBSTICK = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'fctsecure', 'partdump'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'bowie-p0', 'usb_stick_image': None},
+    ],
+}
+
+BREZZA_RESIGN = {
+    'command': BREZZA_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'brezza-p0'},
+        {'board': 'brezza-p0', 'omaha_channel': 'beta-channel'},
+        {'board': 'brezza-p1'},
+        {'board': 'brezza-p1', 'omaha_channel': 'beta-channel'},
+        {'board': 'brezza-p2'},
+        {'board': 'brezza-p2', 'omaha_channel': 'beta-channel'},
+        {'board': 'brezza-b1'},
+        {'board': 'brezza-b1', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+BREZZA_FACTORY = {
+    'command': BREZZA_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'brezza-p1', 'usb_host_package': None},
+        {'board': 'brezza-p2', 'usb_host_package': None},
+    ],
+}
+
+BREZZA_FACTORY_TOOL = {
+    'command':['make'],
+    'factory_tool_types': ['runin2_package', 'provision_package'],
+    'source_tool_path': SOURCE_FACTORY_TOOL_PATH,
+    'runin_profile_path': RUNIN2_PROFILE_PATH
+}
+
+BREZZA_USBSTICK = {
+    'command': BREZZA_RESIGN_COMMAND,
+    'factory_image_types': ['reflash'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'brezza-p2', 'usb_stick_image': None},
+    ],
+}
+
+CAPRICA_RESIGN = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'min_branches': {
+        'caprica-b1': '1.31',
+        'caprica-b2': '1.31',
+        'caprica-b3': '1.31',
+    },
+    'variants': [
+        {'board': 'caprica-b1', 'tz': 'on'},
+        {'board': 'caprica-b1', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'caprica-b2', 'tz': 'on'},
+        {'board': 'caprica-b2', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'caprica-b3', 'tz': 'on'},
+        {'board': 'caprica-b3', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'caprica-b3', 'tz': 'on', 'omaha_channel': 'stable-channel'},
+    ],
+}
+
+CAPRICA_FACTORY = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        # BG2 usbboot prebuilts do not have TZ support.
+        {'board': 'caprica-b1', 'usb_host_package': None, 'tz': 'off'},
+        {'board': 'caprica-b2', 'usb_host_package': None, 'tz': 'off'},
+        {'board': 'caprica-b3', 'usb_host_package': None, 'tz': 'off'},
+        {'board': 'caprica-b4', 'usb_host_package': None, 'tz': 'off'},
+    ],
+}
+
+CAPRICA_USBSTICK = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'fctsecure', 'partdump'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'caprica-b1', 'usb_stick_image': None},
+        {'board': 'caprica-b3', 'usb_stick_image': None},
+    ],
+}
+
+CHORIZO_RESIGN = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'min_branches': {
+        'earth-b1': '1.12',
+        'earth-b2': '1.13',
+        'earth-b3': '1.15',
+    },
+    'variants': [
+        {'board': 'earth-b1', 'tz': 'on'},
+        {'board': 'earth-b2', 'tz': 'on'},
+        {'board': 'earth-b2', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'earth-b2', 'tz': 'on', 'omaha_channel': 'stable-channel'},
+        {'board': 'earth-b3', 'tz': 'on'},
+        {'board': 'earth-b3', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'earth-b3', 'tz': 'on', 'omaha_channel': 'stable-channel'},
+    ],
+}
+
+ELAINE_FUCHSIA_RESIGN = {
+    'command': ELAINE_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'elaine-b3'},
+        {'board': 'elaine-b3', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+ELAINE_RESIGN = {
+    'command': ELAINE_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'elaine-p2', 'always_resign': True},
+        {'board': 'elaine-p2', 'omaha_channel': 'beta-channel'},
+        {'board': 'elaine-b1'},
+        {'board': 'elaine-b1', 'omaha_channel': 'beta-channel'},
+        {'board': 'elaine-b3'},
+        {'board': 'elaine-b3', 'omaha_channel': 'beta-channel'},
+        {'board': 'elaine-b3', 'omaha_channel': 'stable-channel'},
+    ],
+}
+
+ELAINE_FACTORY = {
+    'command': ELAINE_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'elaine-p2', 'usb_host_package': None},
+        {'board': 'elaine-b1', 'usb_host_package': None},
+        {'board': 'elaine-b3', 'usb_host_package': None},
+        {'board': 'elaine-b4', 'usb_host_package': None},
+    ],
+}
+
+ELAINE_USBSTICK = {
+    'command': ELAINE_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'fctsecure', 'partdump'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'elaine-b1', 'usb_stick_image': None},
+        {'board': 'elaine-b3', 'usb_stick_image': None},
+    ],
+}
+
+ESTELLE_FUCHSIA_RESIGN = {
+    'command': ESTELLE_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'estelle-b3'},
+        {'board': 'estelle-b3', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+ESTELLE_RESIGN = {
+    'command': ESTELLE_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'estelle-p2'},
+        {'board': 'estelle-p2', 'omaha_channel': 'beta-channel'},
+        {'board': 'estelle-b1'},
+        {'board': 'estelle-b1', 'omaha_channel': 'beta-channel'},
+        {'board': 'estelle-b3'},
+        {'board': 'estelle-b3', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+ESTELLE_FACTORY = {
+    'command': ESTELLE_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'estelle-b1', 'usb_host_package': None},
+        {'board': 'estelle-b3', 'usb_host_package': None},
+        {'board': 'estelle-b4', 'usb_host_package': None},
+    ],
+}
+
+ESTELLE_USBSTICK = {
+    'command': ESTELLE_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'partdump'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'estelle-b3', 'usb_stick_image': None},
+    ],
+}
+
+EUREKA_RESIGN = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'eureka-b1', 'tz': 'on'},
+    ],
+}
+
+KORLAN_RESIGN = {
+    'command': KORLAN_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'korlan-p2'},
+        {'board': 'korlan-p2', 'omaha_channel': 'beta-channel'},
+        {'board': 'korlan-b1'},
+        {'board': 'korlan-b1', 'omaha_channel': 'beta-channel'},
+        {'board': 'korlan-b3'},
+        {'board': 'korlan-b3', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+KORLAN_FACTORY = {
+    'command': KORLAN_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'korlan-p2', 'usb_host_package': None},
+        {'board': 'korlan-b1', 'usb_host_package': None},
+        {'board': 'korlan-b3', 'usb_host_package': None},
+    ],
+}
+
+KORLAN_FACTORY_TOOL = {
+    'command':['make'],
+    'factory_tool_types': ['runin2_package', 'provision_package'],
+    'source_tool_path': SOURCE_FACTORY_TOOL_PATH,
+    'runin_profile_path': RUNIN2_PROFILE_PATH
+}
+
+KORLAN_USBSTICK = {
+    'command': KORLAN_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'fctsecure'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'korlan-p2', 'usb_stick_image': None},
+        {'board': 'korlan-b1', 'usb_stick_image': None},
+        {'board': 'korlan-b3', 'usb_stick_image': None},
+    ],
+}
+
+LUMIA_RESIGN = {
+    'command': LUMIA_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'lumia-p0'},
+        {'board': 'lumia-p0', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+GQ_RESIGN = {
+    'command': GQ_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'gq-b1',
+         'disabled_branches': ['1.48_shortlived_198292']},
+        {'board': 'gq-b1', 'omaha_channel': 'beta-channel',
+         'disabled_branches': ['1.48_shortlived_198292']},
+        {'board': 'gq-b3',
+         'disabled_branches': ['1.48_shortlived_198292']},
+        {'board': 'gq-b3', 'omaha_channel': 'beta-channel',
+         'disabled_branches': ['1.48_shortlived_198292']},
+    ],
+}
+
+GQ_FACTORY = {
+    'command': GQ_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'gq-b1', 'usb_host_package': None,
+         'disabled_branches': ['1.48_shortlived_198292']},
+        {'board': 'gq-b3', 'usb_host_package': None,
+         'disabled_branches': ['1.48_shortlived_198292']},
+        {'board': 'gq-b4', 'usb_host_package': None,
+         'disabled_branches': ['1.48_shortlived_198292']},
+    ],
+}
+
+GQ_USBSTICK = {
+    'command': GQ_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'partdump', 'fctsecure'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'gq-b1', 'usb_stick_image': None},
+        {'board': 'gq-b3', 'usb_stick_image': None},
+    ],
+}
+
+MUSHROOM_RESIGN = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'min_branches': {
+        'joplin-b1': '1.23',
+        'joplin-b3': '1.26',
+    },
+    'variants': [
+        {'board': 'joplin-b1', 'tz': 'on'},
+        {'board': 'joplin-b1', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'joplin-b1', 'tz': 'on', 'omaha_channel': 'stable-channel'},
+        {'board': 'joplin-b3', 'tz': 'on'},
+        {'board': 'joplin-b3', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'joplin-b3', 'tz': 'on', 'omaha_channel': 'stable-channel'},
+    ],
+}
+
+MUSHROOM_FACTORY = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        # BG2 usbboot prebuilts do not have TZ support.
+        {'board': 'joplin-b1', 'usb_host_package': None, 'tz': 'off'},
+        {'board': 'joplin-b3', 'usb_host_package': None, 'tz': 'off'},
+        {'board': 'joplin-b4', 'usb_host_package': None, 'tz': 'off'},
+    ],
+}
+
+MUSHROOM_USBSTICK = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['reflash'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'joplin-b3', 'usb_stick_image': None}
+    ],
+}
+
+NEWMAN_FUCHSIA_RESIGN = {
+    'command': NEWMAN_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'newman-b3'},
+        {'board': 'newman-b3', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+NEWMAN_RESIGN = {
+    'command': NEWMAN_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'newman-b1'},
+        {'board': 'newman-b1', 'omaha_channel': 'beta-channel'},
+        {'board': 'newman-b3'},
+        {'board': 'newman-b3', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+NEWMAN_RESIGN_ENG = {
+    'command': NEWMAN_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'newman-b1'},
+        {'board': 'newman-b3'},
+    ],
+}
+
+NEWMAN_FACTORY = {
+    'command': NEWMAN_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'newman-b1', 'usb_host_package': None},
+        {'board': 'newman-b3', 'usb_host_package': None},
+        {'board': 'newman-b4', 'usb_host_package': None},
+    ],
+}
+
+NEWMAN_USBSTICK = {
+    'command': NEWMAN_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'fctsecure', 'partdump'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'newman-b1', 'usb_stick_image': None},
+        {'board': 'newman-b3', 'usb_stick_image': None},
+    ],
+}
+
+NQ_RESIGN = {
+    'command': NQ_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'nq-b1',
+         'disabled_branches': ['1.48_shortlived_198292']},
+        {'board': 'nq-b1', 'omaha_channel': 'beta-channel',
+         'disabled_branches': ['1.48_shortlived_198292']},
+        {'board': 'nq-b3',
+         'disabled_branches': ['1.48_shortlived_198292']},
+        {'board': 'nq-b3', 'omaha_channel': 'beta-channel',
+         'disabled_branches': ['1.48_shortlived_198292']},
+    ],
+}
+
+NQ_FACTORY = {
+    'command': NQ_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'nq-b1', 'usb_host_package': None,
+         'disabled_branches': ['1.48_shortlived_198292']},
+        {'board': 'nq-b3', 'usb_host_package': None,
+         'disabled_branches': ['1.48_shortlived_198292']},
+        {'board': 'nq-b4', 'usb_host_package': None,
+         'disabled_branches': ['1.48_shortlived_198292']},
+    ],
+}
+
+NQ_USBSTICK = {
+    'command': NQ_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'partdump', 'fctsecure'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'nq-b1', 'usb_stick_image': None},
+        {'board': 'nq-b3', 'usb_stick_image': None},
+    ],
+}
+
+PEPPERONI_RESIGN = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'min_branches': {
+        'hendrix-b1': '1.14',
+        'hendrix-b3': '1.15',
+    },
+    'variants': [
+        {'board': 'hendrix-b1', 'tz': 'on'},
+        {'board': 'hendrix-b1', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'hendrix-b1', 'tz': 'on', 'omaha_channel': 'stable-channel'},
+        {'board': 'hendrix-b3', 'tz': 'on'},
+        {'board': 'hendrix-b3', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'hendrix-b3', 'tz': 'on', 'omaha_channel': 'stable-channel'},
+    ],
+}
+
+PINEAPPLE_RESIGN = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'min_branches': {
+        'chirp-b1': '1.20',
+        'chirp-b3': '1.21',
+    },
+    'variants': [
+        {'board': 'chirp-b1', 'tz': 'on'},
+        {'board': 'chirp-b1', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'chirp-b1', 'tz': 'on', 'omaha_channel': 'stable-channel'},
+        {'board': 'chirp-b3', 'tz': 'on'},
+        {'board': 'chirp-b3', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'chirp-b3', 'tz': 'on', 'omaha_channel': 'stable-channel'},
+    ],
+}
+
+PINEAPPLE_FACTORY = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        # BG2 usbboot prebuilts do not have TZ support.
+        {'board': 'chirp-b1', 'usb_host_package': None, 'tz': 'off'},
+        {'board': 'chirp-b3', 'usb_host_package': None, 'tz': 'off'},
+        {'board': 'chirp-b4', 'usb_host_package': None, 'tz': 'off'},
+    ],
+}
+
+PINEAPPLE_USBSTICK = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['reflash'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'chirp-b3', 'usb_stick_image': None}
+    ],
+}
+
+PRINCE_RESIGN = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'prince-p2', 'tz': 'on'},
+        {'board': 'prince-p2', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'prince-b1', 'tz': 'on', 'always_resign': True},
+        {'board': 'prince-b1', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'prince-b3', 'tz': 'on'},
+        {'board': 'prince-b3', 'tz': 'on', 'omaha_channel': 'beta-channel'}
+    ],
+}
+
+PRINCE_FACTORY = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'prince-p2', 'usb_host_package': None},
+        {'board': 'prince-b1', 'usb_host_package': None},
+        {'board': 'prince-b3', 'usb_host_package': None},
+        {'board': 'prince-b4', 'usb_host_package': None}
+    ],
+}
+
+PRINCE_USBSTICK = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'fctsecure', 'partdump'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'prince-p2', 'usb_stick_image': None},
+        {'board': 'prince-b1', 'usb_stick_image': None},
+        {'board': 'prince-b3', 'usb_stick_image': None}
+    ],
+}
+
+# Make factory tools eg. provision package
+PRINCE_FACTORY_TOOL = {
+    'command':['make'],
+    'factory_tool_types': ['provision_package'],
+    'source_tool_path': SOURCE_FACTORY_TOOL_PATH
+}
+
+# Use separate factory_tool config as spencer may build runin packages in CB
+SPENCER_FACTORY_TOOL = {
+    'command':['make'],
+    'factory_tool_types': ['provision_package', 'runin2_package'],
+    'source_tool_path': SOURCE_FACTORY_TOOL_PATH,
+    'runin_profile_path': RUNIN2_PROFILE_PATH
+}
+
+VENUS_FACTORY_TOOL = {
+    'command':['make'],
+    'factory_tool_types': ['provision_package', 'runin2_package'],
+    'source_tool_path': SOURCE_FACTORY_TOOL_PATH,
+    'runin_profile_path': RUNIN2_PROFILE_PATH
+}
+
+A049_FACTORY_TOOL = {
+    'command':['make'],
+    'factory_tool_types': ['provision_package', 'runin2_package'],
+    'source_tool_path': SOURCE_FACTORY_TOOL_PATH,
+    'runin_profile_path': RUNIN2_PROFILE_PATH
+}
+
+SALAMI_RESIGN = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'lexx-b3', 'tz': 'on'},
+    ],
+}
+
+SIROCCO_RESIGN = {
+    'command': SIROCCO_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'sirocco-p1'},
+        {'board': 'sirocco-p1', 'omaha_channel': 'beta-channel'},
+        {'board': 'sirocco-b1'},
+        {'board': 'sirocco-b1', 'omaha_channel': 'beta-channel'},
+        {'board': 'sirocco-b3'},
+        {'board': 'sirocco-b3', 'omaha_channel': 'beta-channel'},
+        {'board': 'sirocco-b3', 'omaha_channel': 'stable-channel'},
+    ],
+}
+
+SIROCCO_FACTORY = {
+    'command': SIROCCO_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'sirocco-p1', 'usb_host_package': None},
+        {'board': 'sirocco-b1', 'usb_host_package': None},
+        {'board': 'sirocco-b3', 'usb_host_package': None},
+    ],
+}
+
+SIROCCO_FACTORY_TOOL = {
+    'command':['make'],
+    'factory_tool_types': ['runin2_package', 'provision_package'],
+    'source_tool_path': SOURCE_FACTORY_TOOL_PATH,
+    'runin_profile_path': RUNIN2_PROFILE_PATH
+}
+
+SIROCCO_USBSTICK = {
+    'command': SIROCCO_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'partdump', 'fctsecure'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'sirocco-b1', 'usb_stick_image': None},
+        {'board': 'sirocco-b3', 'usb_stick_image': None},
+    ],
+}
+
+SPENCER_RESIGN = {
+    'command': SPENCER_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'spencer-p1'},
+        {'board': 'spencer-p1u'},
+        {'board': 'spencer-p2'},
+        {'board': 'spencer-p2', 'omaha_channel': 'beta-channel'},
+        {'board': 'spencer-b1'},
+        {'board': 'spencer-b1', 'omaha_channel': 'beta-channel'},
+        {'board': 'spencer-b3'},
+        {'board': 'spencer-b3', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+SPENCER_FACTORY = {
+    'command': SPENCER_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'spencer-p1', 'usb_host_package': None},
+        {'board': 'spencer-p1u', 'usb_host_package': None},
+        {'board': 'spencer-p2', 'usb_host_package': None},
+        {'board': 'spencer-b1', 'usb_host_package': None},
+        {'board': 'spencer-b3', 'usb_host_package': None},
+    ],
+}
+
+SPENCER_USBSTICK = {
+    'command': SPENCER_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'partdump', 'fctsecure'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'spencer-p2', 'usb_stick_image': None},
+        {'board': 'spencer-b1', 'usb_stick_image': None},
+        {'board': 'spencer-b3', 'usb_stick_image': None},
+    ],
+}
+
+SPRINKLES_RESIGN = {
+    'command': SPRINKLES_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'sprinkles-korlan'},
+        {'board': 'sprinkles-korlan', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+SPRINKLES_FACTORY = {
+    'command': SPRINKLES_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'sprinkles-korlan', 'usb_host_package': None},
+    ],
+}
+
+SPRINKLES_FACTORY_TOOL = {
+    'command':['make'],
+    'factory_tool_types': ['runin2_package', 'provision_package'],
+    'source_tool_path': SOURCE_FACTORY_TOOL_PATH,
+    'runin_profile_path': RUNIN2_PROFILE_PATH
+}
+
+SPRINKLES_USBSTICK = {
+    'command': SPRINKLES_RESIGN_COMMAND,
+    'factory_image_types': ['reflash'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'sprinkles-korlan', 'usb_stick_image': None},
+    ],
+}
+
+STEAK_RESIGN = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'min_branches': {
+        'luther-b1': '1.20',
+        'luther-b2': '1.21',
+    },
+    'variants': [
+        {'board': 'luther-b2', 'tz': 'on'},
+        {'board': 'luther-b2', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'luther-b2', 'tz': 'on', 'omaha_channel': 'stable-channel'},
+    ],
+}
+
+STEAK_FACTORY = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        # bg4cdp USB boot host tool supports TZ
+        {'board': 'luther-b2', 'usb_host_package': None},
+        {'board': 'luther-b4', 'usb_host_package': None},
+    ],
+}
+
+STEAK_USBSTICK = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'fctsecure', 'partdump'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'luther-b2', 'usb_stick_image': None}
+    ],
+}
+
+TQ_RESIGN = {
+    'command': TQ_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'tq-p1'},
+        {'board': 'tq-p2'},
+        {'board': 'tq-p2', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+TQ_FACTORY = {
+    'command': TQ_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'tq-p1', 'usb_host_package': None},
+        {'board': 'tq-p2', 'usb_host_package': None},
+    ],
+}
+
+VALENS_RESIGN = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'valens-p2', 'tz': 'on'},
+        {'board': 'valens-p2', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'valens-b1', 'tz': 'on'},
+        {'board': 'valens-b1', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'valens-b3', 'tz': 'on'},
+        {'board': 'valens-b3', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+VALENS_FACTORY = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'valens-p2', 'usb_host_package': None},
+        {'board': 'valens-b1', 'usb_host_package': None},
+        {'board': 'valens-b3', 'usb_host_package': None},
+        {'board': 'valens-b4', 'usb_host_package': None},
+    ],
+}
+
+# uNAND step require fct zip created first
+VALENS_UNAND = {
+    'command': VALENS_UNAND_COMMAND,
+    'factory_image_types': ['unand'],
+    'source_binary_path': FCT_ZIP_PATH,
+    'variants': [
+        {'board': 'valens-p2'},
+        {'board': 'valens-b1'},
+        {'board': 'valens-b3'},
+        {'board': 'valens-b4'},
+    ],
+}
+
+VALENS_USBSTICK = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'fctsecure', 'partdump'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'valens-b3', 'usb_stick_image': None}
+    ],
+}
+
+VENTO_RESIGN = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'vento-p0', 'tz': 'on'},
+        {'board': 'vento-p0', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'vento-b1', 'tz': 'on'},
+        {'board': 'vento-b1', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+        {'board': 'vento-b3', 'tz': 'on'},
+        {'board': 'vento-b3', 'tz': 'on', 'omaha_channel': 'beta-channel'},
+    ],
+}
+
+VENTO_FACTORY = {
+    'command': MARVELL_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'vento-p0', 'usb_host_package': None},
+        {'board': 'vento-b1', 'usb_host_package': None},
+        {'board': 'vento-b3', 'usb_host_package': None},
+        {'board': 'vento-b4', 'usb_host_package': None},
+    ],
+}
+
+# uNAND step require fct zip created first
+VENTO_UNAND = {
+    'command': VENTO_UNAND_COMMAND,
+    'factory_image_types': ['unand'],
+    'source_binary_path': FCT_ZIP_PATH,
+    'variants': [
+        {'board': 'vento-p0'},
+        {'board': 'vento-b1'},
+        {'board': 'vento-b3'},
+        {'board': 'vento-b4'},
+    ],
+}
+
+VENUS_RESIGN = {
+    'command': VENUS_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'venus-p1'},
+        {'board': 'venus-p1', 'omaha_channel': 'beta-channel'},
+        {'board': 'venus-p2'},
+        {'board': 'venus-p2', 'omaha_channel': 'beta-channel'},
+        {'board': 'venus-b1',
+         'disabled_branches': ['1.57_shortlived_265478_vfctp2']},
+        {'board': 'venus-b1', 'omaha_channel': 'beta-channel',
+         'disabled_branches': ['1.57_shortlived_265478_vfctp2']},
+        {'board': 'venus-b3',
+         'disabled_branches': ['1.57_shortlived_265478_vfctp2']},
+        {'board': 'venus-b3', 'omaha_channel': 'beta-channel',
+         'disabled_branches': ['1.57_shortlived_265478_vfctp2']},
+    ],
+}
+
+VENUS_FACTORY = {
+    'command': VENUS_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'venus-p1', 'usb_host_package': None},
+        {'board': 'venus-p2', 'usb_host_package': None},
+        {'board': 'venus-b1', 'usb_host_package': None,
+         'disabled_branches': ['1.57_shortlived_265478_vfctp2']},
+        {'board': 'venus-b3', 'usb_host_package': None,
+         'disabled_branches': ['1.57_shortlived_265478_vfctp2']},
+    ],
+}
+
+VENUS_USBSTICK = {
+    'command': VENUS_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'partdump', 'fctsecure'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'venus-p2', 'usb_stick_image': None},
+        {'board': 'venus-b1', 'usb_stick_image': None,
+         'disabled_branches': ['1.57_shortlived_265478_vfctp2']},
+        {'board': 'venus-b3', 'usb_stick_image': None,
+         'disabled_branches': ['1.57_shortlived_265478_vfctp2']},
+    ],
+}
+
+A049_RESIGN = {
+    'command': A049_RESIGN_COMMAND,
+    'variants': [
+        {'board': 'a049-p0'},
+    ],
+}
+
+A049_FACTORY = {
+    'command': A049_RESIGN_COMMAND,
+    'factory_image_types': ['fct'],
+    'source_binary_path': SOURCE_FACTORY_PATH,
+    'variants': [
+        {'board': 'a049-p0', 'usb_host_package': None},
+    ],
+}
+
+A049_USBSTICK = {
+    'command': A049_RESIGN_COMMAND,
+    'factory_image_types': ['reflash', 'partdump', 'fctsecure'],
+    'source_binary_path': SOURCE_USBSTICK_PATH,
+    'destination_path_template': DESTINATION_USBSTICK_IMAGE_PATH,
+    'variants': [
+        {'board': 'a049-p0', 'usb_stick_image': None},
+    ],
+}
+
+def get_resign_steps(build_name, resign_config, build_branch,
+                     apply_dev_signing, **step_kwargs):
+  """Returns list of resign steps if any.
+
+  Args:
+    build_name: Name of build (e.g. anchovy-eng, tvdefault-eng).
+    resign_config: dictionary with resign params.
+    build_branch: manifest branch for current buildset.
+    apply_dev_signing: Ingnore per-board dev_signing param if True.
+    **step_kwargs: kwargs for step constructor.
+  Returns:
+    List of OtaResignStep objects if any
+  """
+  if not resign_config or 'command' not in resign_config:
+    return []
+
+  steps = []
+
+  min_branches = resign_config.get('min_branches', {})
+  for variant in resign_config.get('variants', []):
+    if build_branch in variant.get('disabled_branches', []):
+      continue
+    params = variant.copy()
+    if 'disabled_branches' in params:
+      params.pop('disabled_branches')
+    product = params.get('board', '')
+    resign_variant = apply_dev_signing or params.get('always_resign', False)
+    if 'always_resign' in params:
+      params.pop('always_resign')
+    if not resign_variant:
+      continue
+    if branch_utils.is_branch_equal_to_or_later_than(
+        build_branch, min_branches.get(product, None)):
+      for factory_image_type in resign_config.get(
+          'factory_image_types', [None]):
+        if factory_image_type == 'unand':
+          steps.append(OtaUnandStep(
+              build_name, command=resign_config['command'],
+              params=params,
+              source_binary_path=resign_config.get(
+                  'source_binary_path', FCT_ZIP_PATH),
+              **step_kwargs
+          ))
+        else:
+          steps.append(OtaResignStep(
+              build_name, command=resign_config['command'],
+              params=params, factory_image_type=factory_image_type,
+              source_binary_path=resign_config.get(
+                  'source_binary_path', SOURCE_BINARY_PATH),
+              destination_path_template=resign_config.get(
+                  'destination_path_template', None),
+              include_params=resign_config.get('include_params', True),
+              **step_kwargs
+          ))
+
+  return steps
+
+
+class OtaResignStep(ota_step.OtaStep):
+  """Resign step class for building OTA's."""
+
+  def __init__(self, build_name,
+               command, params, source_binary_path, include_params,
+               destination_path_template=None, name=None,
+               factory_image_type=None, **kwargs):
+    """Creates a OtaResignStep instance.
+
+    Args:
+      build_name: Name of build (e.g. anchovy-eng, tvdefault-eng)
+      command: resign command, list
+      params: resign params, dictionary
+      source_binary_path: path to source binary, string
+      include_params: if True, params will be converted command flags/values
+      destination_path_template: string. If None, a default template is used.
+      name: user-visible name of this step.
+      factory_image_type: Type of factory image, None otherwise
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    if not name:
+      name = 'resign '
+      if factory_image_type:
+        name += factory_image_type + ' '
+      name += params['board']
+
+    tz = ''
+    if params.get('tz') == 'off':
+      tz = '-no-tz'
+      name += ' no trustzone'
+    elif params.get('tz') == 'on':
+      name += ' trustzone'
+
+    if 'omaha_channel' in params:
+      channel = '-' + params['omaha_channel']
+      name += ' ' + params['omaha_channel']
+    else:
+      channel = ''
+
+    ota_step.OtaStep.__init__(self, build_name=build_name, name=name, **kwargs)
+    product = self._build_name.split('-')[0]
+
+    if destination_path_template is None:
+      if factory_image_type:
+        destination_path_template = ('{output_dir}/factory/{product}-'
+                                     '{image_type}-{variant}-{buildset}.zip')
+      else:
+        destination_path_template = ('{output_dir}/{product}-ota-{variant}'
+                                     '{tz}{channel}-{buildset}.zip')
+    self._resign_command = command[:]
+    if include_params:
+      for flag in params.keys():
+        param = params[flag]
+        if param:
+          self._resign_command += ['--' + flag, param]
+        else:
+          self._resign_command += ['--' + flag]
+
+    self._resign_command.append(source_binary_path.format(
+        dist_dir=self._dist_dir(),
+        product=product,
+        image_type=factory_image_type,
+        buildset=self.build_number))
+
+    self._resign_command.append(destination_path_template.format(
+        output_dir=self._ota_archive_output_dir(), product=product,
+        buildset=self.build_number,
+        image_type=factory_image_type,
+        variant=params['board'], channel=channel, tz=tz))
+
+  def run(self):
+    returncode, stdout, stderr = self.exec_subprocess(self._resign_command)
+    if returncode != 0:
+      self._error_processor(stdout, stderr)
+
+    return returncode == 0
+
+
+class OtaUnandStep(OtaResignStep):
+  """Resign step class for building OTA's."""
+
+  # pylint: disable=super-init-not-called
+  def __init__(self, build_name,
+               command, params, source_binary_path,
+               name=None, **kwargs):
+    """Creates a OtaResignStep instance.
+
+    Args:
+      build_name: Name of build (e.g. anchovy-eng, tvdefault-eng)
+      command: resign command, list
+      params: resign params, dictionary
+      source_binary_path: path to source binary, string
+      name: user-visible name of this step.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    if not name:
+      name = 'resign uNAND '
+      name += params['board']
+
+    # pylint: disable=non-parent-init-called
+    ota_step.OtaStep.__init__(self, build_name=build_name, name=name, **kwargs)
+
+    product = self._build_name.split('-')[0]
+    self._resign_command = command[:]
+    self._resign_command.append(params['board'])
+    self._resign_command.append(source_binary_path.format(
+        dist_dir=self._dist_dir(),
+        product=product,
+        buildset=self.build_number))
+    self._resign_command.append('{output_dir}/factory'.format(
+        output_dir=self._ota_archive_output_dir()))
diff --git a/cq/scripts/slave/step/ota_step.py b/cq/scripts/slave/step/ota_step.py
new file mode 100644
index 0000000..d50b68d
--- /dev/null
+++ b/cq/scripts/slave/step/ota_step.py
@@ -0,0 +1,607 @@
+"""Build step class for building OTA's."""
+
+from __future__ import absolute_import
+import logging
+import os
+import re
+import sys
+
+from helpers import make_utils
+from helpers import ninja_utils
+from helpers import py_env_utils
+from slave import base_step
+
+# Taken from recommended value in autoninja
+BUILD_ACCELERATION_JOB_MULTIPLIER = 40
+
+# Allow sufficient time for builds without build acceleration to finish.
+_OTA_STEP_MAX_TIME_SECONDS = 6 * 60 * 60
+
+_MESH_TAG = '-mesh-'
+_YETI_TAG = '-yeti-'
+_SDM_TAG = '-sdm-'
+_GALLIUM_TAG = '-gallium-'
+_OPAL_TAG = '-opal-'
+_CONCIERGE_TAG = '-concierge-'
+_HEALTH_TAG = '-health-'
+_FUCHSIA_TAG = '-fuchsia-'
+_FUCHSIA_REV_TAG = '-fuchsia_rev-'
+_ML_FRAMEWORK_TAG = '-ml_framework-'
+_DEMO_TAG = '-demo-'
+_OTA_FILES_RE = r'.*-ota-.*\..*'
+_PARTNER_TAG = '-partner-'
+_LEGACY_TAG = '_legacy'
+_1LED_TAG = '-1led-'
+_MODULAR_COMMS_TAG = '-modular_comms-'
+_IOT_CAST_TAG = '-iot_cast-'
+
+GANETI_TAG = '-ganeti-'
+
+_OTA_ZIP_TEMPLATE = '{}-ota-{}.zip'
+
+
+class OtaStep(base_step.BaseStep):
+  """Build step class for building OTA's."""
+
+  def __init__(self, build_name, name='build ota', board_name=None,
+               extra_params=None, max_ota_size=None, target_name=None,
+               timeout_secs=_OTA_STEP_MAX_TIME_SECONDS,
+               **kwargs):
+    """Creates a OtaStep instance.
+
+    Args:
+      build_name: Name of build (e.g. anchovy-eng, tvdefault-eng)
+      name: user-visible name of this step.
+      board_name: optional BOARD_NAME param for make if any.
+      extra_params: optional list of params to add
+      max_ota_size: Max size of OTA to enfoce if any enforcement required.
+      timeout_secs: Max execution time for OTA steps.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name=name, timeout_secs=timeout_secs,
+                                **kwargs)
+    self._build_name = build_name
+    self._board_name = board_name
+    self._extra_params = extra_params
+    self._max_ota_size = max_ota_size
+    self._target_name = target_name
+
+    self._enable_verbose_logging = self._properties.get(
+      'enable_verbose_logging', False)
+
+    self._is_mesh_build = _MESH_TAG in self._build_name
+    self._build_name = self._build_name.replace(_MESH_TAG, '-')
+
+    self._is_yeti_build = _YETI_TAG in self._build_name
+    self._build_name = self._build_name.replace(_YETI_TAG, '-')
+
+    self._is_sdm_build = _SDM_TAG in self._build_name
+    self._build_name = self._build_name.replace(_SDM_TAG, '-')
+
+    self._is_gallium_build = _GALLIUM_TAG in self._build_name
+    self._build_name = self._build_name.replace(_GALLIUM_TAG, '-')
+
+    self._is_opal_build = _OPAL_TAG in self._build_name
+    self._build_name = self._build_name.replace(_OPAL_TAG, '-')
+
+    self._is_concierge_build = _CONCIERGE_TAG in self._build_name
+    self._build_name = self._build_name.replace(_CONCIERGE_TAG, '-')
+
+    self._is_health_build = _HEALTH_TAG in self._build_name
+    self._build_name = self._build_name.replace(_HEALTH_TAG, '-')
+
+    self._is_fuchsia_build = _FUCHSIA_TAG in self._build_name
+    self._build_name = self._build_name.replace(_FUCHSIA_TAG, '-')
+
+    self._is_fuchsia_rev_build = _FUCHSIA_REV_TAG in self._build_name
+    self._build_name = self._build_name.replace(_FUCHSIA_REV_TAG, '-')
+
+    self._is_ml_framework_build = _ML_FRAMEWORK_TAG in self._build_name
+    self._build_name = self._build_name.replace(_ML_FRAMEWORK_TAG, '-')
+
+    self._is_demo_build = _DEMO_TAG in self._build_name
+    self._build_name = self._build_name.replace(_DEMO_TAG, '-')
+
+    self._is_ganeti_build = GANETI_TAG in self._build_name
+    self._build_name = self._build_name.replace(GANETI_TAG, '-')
+
+    self._is_1led_build = _1LED_TAG in self._build_name
+    self._build_name = self._build_name.replace(_1LED_TAG, '-')
+
+    self._is_modular_comms_build = _MODULAR_COMMS_TAG in self._build_name
+    self._build_name = self._build_name.replace(_MODULAR_COMMS_TAG, '-')
+
+    self._is_iot_cast_build = _IOT_CAST_TAG in self._build_name
+    self._build_name = self._build_name.replace(_IOT_CAST_TAG, '-')
+
+    # Partner repo is specified in properties, but the tag needs to be
+    # removed from the build name.
+    self._build_name = self._build_name.replace(_PARTNER_TAG, '-')
+
+    self._build_name = self._build_name.replace(_LEGACY_TAG, '')
+
+  def _dist_dir(self):
+    dist_dir = os.path.join('out/dist', self.build_number)
+    if self.patchset != '0':
+      dist_dir = os.path.join(dist_dir, str(self.patchset))
+    return dist_dir
+
+  def make_target_param(self, target_params=False):
+    """Creates PRODUCT or TARGET params for make.
+
+    build/build test steps are using PRODUCT-<build name> notation,
+    TARGET_PRODUCT and TARGET_BUILD_VARIANT params required for OSS and FCT.
+
+    Args:
+      target_params: use TARGET style params regardless current build_system.
+    Returns:
+      array of params to be appended to make command.
+    """
+    target_name = self._target_name
+    if not target_name:
+      target_name = self._build_name
+    if target_params:
+      product, variant = target_name.split('-')
+      return ['TARGET_PRODUCT={}'.format(product),
+              'TARGET_BUILD_VARIANT={}'.format(variant)]
+    return ['PRODUCT-' + target_name]
+
+  def make_command_env(self):
+    """Generates env dictionary for make command."""
+    return self.build_accelerator.environment_variables
+
+  def _make_extra_flags(self):
+    """Generates make extra params based on build type."""
+
+    flags = []
+    variant = self._build_name.split('-')[-1]
+    if variant != 'eng':
+      flags += ['OFFICIAL_BUILD=1']
+    if self._enable_verbose_logging:
+      flags.append('showcommands')
+    if self._is_mesh_build:
+      flags += ['USE_GWIFI=1']
+    if self._is_yeti_build:
+      flags += ['ENABLE_YETI=1', 'ENABLE_CLASSIC_YETI=0']
+    if self._is_sdm_build:
+      flags += ['ENABLE_HOSPITALITY=1', 'ENABLE_MANAGED_MODE=1']
+    if self._is_gallium_build:
+      flags += ['ENABLE_GALLIUM=1']
+    if self._is_opal_build:
+      flags += ['ENABLE_OPAL=true']
+    if self._is_concierge_build:
+      flags += ['ENABLE_SOUND_PRESENCE=1']
+    if self._is_health_build:
+      flags += ['ENABLE_HEALTH_SOUND=1', 'ENABLE_MANAGED_MODE=1']
+    if self._is_fuchsia_build:
+      flags += ['FUCHSIA_OTA=true']
+    if self._is_fuchsia_rev_build:
+      flags += ['FUCHSIA_REVERSE=true']
+    if self._is_ml_framework_build:
+      flags += ['ENABLE_ML_FRAMEWORK=true']
+    if self._is_demo_build:
+      flags += ['DEMO_OTA=true']
+    if self._board_name:
+      flags += ['BOARD_NAME={}'.format(self._board_name)]
+    if self._is_1led_build:
+      flags += ['NUM_LEDS=1']
+    if self._is_modular_comms_build:
+      flags += ['ENABLE_MODULAR_COMMS=true']
+    if self._is_iot_cast_build:
+      flags += ['ENABLE_IOT_CAST=1']
+    if self._extra_params:
+      flags += self._extra_params
+    return flags
+
+  def _build_ota(self, custom_shell):
+    """Builds |build_name| OTA package for the |issue| and |patchset|.
+
+    Args:
+      custom_shell: List containing custom shell, to be used with make.
+    Returns:
+      True if the OTA package was built successfully, False otherwise.
+    """
+    # Clean the systemimage first to avoid bleedover on incremental builds
+    # from previous builds of the same product.
+    command = ['make',
+               'BUILD_NUMBER={}'.format(self.build_number),
+               'USER=auto',
+               'clean-systemimage',
+               'DIST_DIR={}'.format(self._dist_dir())]
+    command += self.build_accelerator.make_flags
+
+    quiet = False
+    if custom_shell:
+      quiet = True
+
+    # For this step, don't use PRODUCT-* because it adds a "dist" goal. Just
+    # perform the clean-systemimage.
+    command += self.make_target_param(target_params=True)
+    command += self._make_extra_flags()
+    returncode, stdout, stderr = self.exec_subprocess(
+        command, env=self.make_command_env(), quiet=quiet)
+
+    # We permit this step to fail because the clean-systemimage step
+    # is custom to Cast OS and does not exist on older release branches.
+    # Since CQ build logic always runs on master, regardless of the age
+    # of the code being built, we must maintain backwards compatibility.
+    if returncode != 0:
+      logging.warning(
+        'Failed to run clean-systemimage. This is normal on release branches '
+        'from 1.50 and earlier. If this happens on a newer branch it is an '
+        'error and will cause non-deterministic ota package sizes.\n'
+        'Return code %d, stderr:\n%s',
+        returncode, stderr)
+
+
+    command = ['make',
+               '--output-sync=target',
+               '-j{}'.format(self.get_num_jobs(
+                   multiplier=BUILD_ACCELERATION_JOB_MULTIPLIER)),
+               'BUILD_NUMBER={}'.format(self.build_number),
+               'USER=auto',
+               'dist',
+               'DIST_DIR={}'.format(self._dist_dir()),
+               'DISABLE_AUTO_INSTALLCLEAN=true',]
+    command += self.build_accelerator.make_flags
+    command += self.make_target_param()
+    command += self._make_extra_flags()
+
+    if custom_shell:
+      command += custom_shell
+
+    returncode, stdout, stderr = self.exec_subprocess(
+        command, env=self.make_command_env(), quiet=quiet)
+
+    if custom_shell:
+      self._merge_build_logs()
+
+    if returncode != 0:
+      if custom_shell:
+        self._dump_error_logs(stdout, stderr)
+      self._error_processor(stdout, stderr)
+    else:
+      if custom_shell:
+        self._print_build_success()
+
+    if custom_shell:
+      sys.stdout.write('Complete logs can be found in build_ota.log '
+      'within invocation artifacts, sorted by module. Logs that are '
+      'generated outside of a module are logged under the "general" module.\n')
+      sys.stdout.flush()
+
+    return returncode == 0
+
+  def _error_processor(self, stdout, stderr):
+    messages = ninja_utils.ninja_failure_to_comments(stdout, max_errors=3)
+    for message in messages:
+      self.add_review({'message': message})
+
+    comments = make_utils.make_error_to_comments(stderr, self.directory)
+    if comments:
+      self.add_review({'comments': comments})
+
+  def _print_build_success(self):
+    sys.stdout.write('Build successful.\n')
+    sys.stdout.flush()
+
+  def _clear_dist(self, quiet):
+    """Clears everything in the dist directory leftover from previous builds.
+    Args:
+      quiet: Suppress subprocess output.
+    """
+    dist_dirs = self.glob_files('out/dist/*')
+    if dist_dirs:
+      self.exec_subprocess(['rm', '-rf'] + dist_dirs, quiet=quiet)
+
+  def _clear_ota_artifacts(self, quiet):
+    """Removes all OTA artifacts leftover by previous builds.
+    Args:
+      quiet: Suppress subprocess output.
+    """
+    archive_pattern = re.compile(_OTA_FILES_RE)
+
+    dist_files = self.glob_files('{}/*'.format(self._dist_dir()))
+    ota_files = [f for f in dist_files if archive_pattern.match(f)]
+    if ota_files:
+      self.exec_subprocess(['rm', '-f'] + ota_files, quiet=quiet)
+
+  def _clear_symbols_artifacts(self, quiet):
+    """Removes all symbols artifacts leftover by previous builds.
+    Args:
+      quiet: Suppress subprocess output.
+    """
+    symbols_files = self.glob_files('out/target/product/*/*-symbols-*.zip')
+    if symbols_files:
+      self.exec_subprocess(['rm', '-f'] + symbols_files, quiet=quiet)
+
+  def _ota_archive_output_dir(self):
+    """Return system specific archive dir."""
+    output_dir = self.get_gcs_dir()
+    if self.build_system != 'catabuilder':
+      output_dir = os.path.join(output_dir, 'artifacts')
+      if not os.path.exists(output_dir):
+        os.makedirs(output_dir)
+
+    return output_dir
+
+  def _archive_artifacts(self, quiet):
+    """Copies the artifacts for archival.
+    Args:
+      quiet: Suppress subprocess output.
+    """
+    output_dir = self._ota_archive_output_dir()
+    if self.build_system == 'catabuilder':
+      # Archive everything in the continuous build system.
+      archive_pattern = re.compile(r'.*')
+    else:
+      archive_pattern = re.compile(_OTA_FILES_RE)
+
+    dist_files = self.glob_files('{}/*'.format(self._dist_dir()))
+    archive_files = [f for f in dist_files if archive_pattern.match(f)]
+    for f in archive_files:
+      self.exec_subprocess(['cp', '-r', f, output_dir], quiet=quiet)
+
+    # Save the OTA file list for later use.
+    ota_files = [f for f in archive_files if re.match(_OTA_FILES_RE, f)]
+    if ota_files:
+      filenames = [os.path.basename(x) for x in ota_files]
+      self.set_build_property('ota_files', filenames)
+
+  def _dump_error_logs(self, stdout, stderr):
+    """Dumps error logs to stderr."""
+    product = self._build_name.split('-')[0]
+    error_log_files = sorted(self.glob_files(
+      'out/target/product/' + product + '/build_logs/*.err.txt'),
+      key=os.path.basename)
+
+    if error_log_files:
+      sys.stdout.write('The following modules failed to build:\n')
+      sys.stdout.flush()
+      for error_file in error_log_files:
+        with open(error_file, 'r') as err_file:
+          contents = err_file.read()
+          sys.stdout.write(os.path.basename(error_file).split('.')[0] + ':\n')
+          sys.stdout.flush()
+          sys.stderr.write(contents)
+          sys.stderr.flush()
+    else:
+      # No errors found, dump error logs to test.log.
+      sys.stderr.write(stderr)
+      sys.stderr.flush()
+
+  def _merge_build_logs(self):
+    """Copies build logs for archival."""
+    product = self._build_name.split('-')[0]
+    build_log_files = sorted(self.glob_files(
+      'out/target/product/' + product + '/build_logs/*.out.txt'),
+      key=os.path.basename)
+    output_dir = self.get_gcs_dir()
+    if not os.path.exists(output_dir):
+      os.makedirs(output_dir)
+    build_logs_file = os.path.join(output_dir, 'build_ota.log')
+    with open(build_logs_file, 'w') as out_file:
+      for log_file in build_log_files:
+        with open(log_file, 'r') as in_file:
+          contents = in_file.read()
+          if contents:
+            out_file.write('**********************************'
+              '************************************\n');
+            out_file.write(os.path.basename(log_file).split('.')[0] + ':\n')
+            out_file.write('**********************************'
+              '************************************\n');
+            out_file.write(contents + '\n')
+      out_file.flush()
+
+  def _check_ota_size(self):
+    """Checks the size of the OTA.
+
+    Returns:
+      Boolean if the OTA is under the max size.
+    """
+    # TODO(gfhuang): A/B ota size check need rework.
+    # This as-is checks both .zip and .bin.
+    archive_pattern = re.compile(_OTA_FILES_RE)
+
+    dist_files = self.glob_files('{}/*'.format(self._dist_dir()))
+    ota_files = [f for f in dist_files if archive_pattern.match(f)]
+    self._log_ota_size(ota_files)
+    if not self._max_ota_size:
+      return True
+
+    for ota_file in ota_files:
+      ota_size = os.path.getsize(ota_file)
+      if ota_size > self._max_ota_size:
+        message = 'OTA size of {} exceed max of {} for file {}'.format(
+            ota_size, self._max_ota_size, ota_file)
+        sys.stdout.write(message + '\n')
+        sys.stdout.flush()
+        self.add_review({'message': message})
+        return False
+    return True
+
+  def _log_ota_size(self, ota_files):
+    target_ota_filename = _OTA_ZIP_TEMPLATE.format(
+        self.get_property('buildername', default='').split('-')[0],
+        self.get_property('buildset'))
+    ota_file = next((f for f in ota_files
+                     if os.path.basename(f) == target_ota_filename), None)
+
+    if not ota_file:
+      return
+
+    self.set_build_property('ota_size', os.path.getsize(ota_file))
+    if self._max_ota_size:
+      self.set_build_property('max_ota_size', self._max_ota_size)
+
+  def _make_custom_shell(self):
+    """Returns a list containing a path to the custom shell"""
+    shell_path = os.path.abspath(os.path.join(
+                    os.getcwd(),
+                    'build/android-mk-module-split-shell'))
+    if os.path.isfile(shell_path):
+      return ['ANDROID_BUILD_SHELL={}'.format(shell_path)]
+    return []
+
+  def run(self):
+    """Builds ota package.
+
+    Returns:
+      True iff there were no errors.
+    """
+    custom_shell = self._make_custom_shell()
+    self._clear_dist(bool(custom_shell))
+    self._clear_ota_artifacts(bool(custom_shell))
+    self._clear_symbols_artifacts(bool(custom_shell))
+    if not self._build_ota(custom_shell):
+      return False
+    self._archive_artifacts(bool(custom_shell))
+    if not self._check_ota_size():
+      return False
+    return True
+
+
+class OtaPropStep(base_step.BaseStep):
+  """Ota Property file step class for generating repo.prop."""
+
+  def __init__(self, **kwargs):
+    """Creates a OtaPropStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='save repo.prop', **kwargs)
+
+  def run(self):
+    prop_cmd = [
+        'repo', 'forall', '-c',
+        'git show -s --format="$REPO_PROJECT %H"'
+    ]
+    proj_sha_pair_re = re.compile(r'^.+? [0-9a-f]{40}$', flags=re.IGNORECASE)
+
+    repo_prop = self.exec_subprocess(prop_cmd, check_output=True, quiet=True)
+    valid_lines = [l for l in repo_prop.split('\n')
+                   if proj_sha_pair_re.match(l.strip())]
+    prop_file = os.path.join(self.get_gcs_dir(), 'repo.prop')
+    with open(prop_file, 'w') as f:
+      f.write('\n'.join(valid_lines))
+    return True
+
+class OtaCameraTargetsStep(base_step.BaseStep):
+  """Ota Property file step class for generating repo.prop."""
+
+  def __init__(self, product_name, is_eng_build, name='camera targets check',
+               **kwargs):
+    """Creates a OtaCameraTargets instance.
+
+    Args:
+      product_name: product name without variant, e.g. spencer, venus
+      is_eng_build: if this build is eng build
+      name: user-visible name of this step.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='camera targets check', **kwargs)
+    self._is_eng_build = is_eng_build
+    self._product_name = product_name
+
+  def run(self):
+    eng_targets = [
+      '/system/bin/camera_tool',
+      '/system/bin/ffprobe',
+      '/system/bin/ffmpeg',
+      '/system/bin/cast_camera_hal_test'
+    ]
+    general_targets = [ '/obj/lib/libcast_camera_hal.so' ]
+    if self._is_eng_build:
+      for t in eng_targets:
+        abs_path = 'out/target/product/' + self._product_name + t
+        if not os.path.exists(abs_path):
+          logging.error('Missing binary %s' % abs_path)
+          return False
+    for b in general_targets:
+      abs_path = 'out/target/product/' + self._product_name + b
+      if not os.path.exists(abs_path):
+        logging.error('Missing binary %s' % abs_path)
+        return False
+    return True
+
+
+class OtaUsoniaDaemonStep(OtaStep):
+  """Ota Property file step class for generating repo.prop."""
+
+  def __init__(self, build_name, **kwargs):
+    """Creates a OtaPropStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    OtaStep.__init__(self, build_name=build_name,
+                     name='build usonia_daemon', **kwargs)
+
+  def run(self):
+    """Builds |usonia_daemon| for the |issue| and |patchset|.
+    Returns:
+      True if the usonia_daemon package is successfully built.
+    """
+
+    chromium_src = 'chromium/src'
+    usonia_tarball = os.path.join(chromium_src,
+                                  'usonia-{}.tar.gz'.format(self.build_number))
+    command = ['chromecast/internal/usonia/package_wifi.py', '-o',
+               '-f', '.', self.build_number]
+    returncode, stdout, stderr = self.exec_subprocess(
+      command, env=self.make_command_env(), cwd=chromium_src)
+    if returncode != 0:
+      self.exec_subprocess(['rm', '-rf', usonia_tarball])
+      self._error_processor(stdout, stderr)
+      return False
+
+    archive_dir = self._ota_archive_output_dir()
+    returncode, stdout, stderr = self.exec_subprocess(
+      ['cp', '-r', usonia_tarball, archive_dir])
+
+    self.exec_subprocess(['rm', '-rf', usonia_tarball])
+    if returncode != 0:
+      self._error_processor(stdout, stderr)
+      return False
+
+    return True
+
+
+class ThreadTelemetryServiceCrosStep(OtaStep):
+  """Build step class for building Thread Telemetry Service for use in Chrome
+  OS."""
+
+  def __init__(self, **kwargs):
+    OtaStep.__init__(self, name='build thread_telemetry_service_cros', **kwargs)
+
+  def run(self):
+    chromium_src = 'chromium/src'
+    tarball = os.path.join(chromium_src,
+                           'thread_telemetry_service-{}.tar.gz'.format(
+                             self.build_number))
+    command = [
+      'chromecast/internal/device/thread/'
+      'build_thread_telemetry_service_cros.py',
+      '-o',
+      '-f',
+      '.',
+      self.build_number,
+    ]
+    returncode, stdout, stderr = self.exec_subprocess(
+      command, env=self.make_command_env(), cwd=chromium_src)
+    if returncode != 0:
+      self.exec_subprocess(['rm', '-f', tarball])
+      self._error_processor(stdout, stderr)
+      return False
+
+    archive_dir = self._ota_archive_output_dir()
+    returncode, stdout, stderr = self.exec_subprocess(
+      ['cp', '-r', tarball, archive_dir])
+
+    self.exec_subprocess(['rm', '-f', tarball])
+    if returncode != 0:
+      self._error_processor(stdout, stderr)
+      return False
+
+    return True
diff --git a/cq/scripts/slave/step/ota_strip_binaries_step.py b/cq/scripts/slave/step/ota_strip_binaries_step.py
new file mode 100644
index 0000000..7170159
--- /dev/null
+++ b/cq/scripts/slave/step/ota_strip_binaries_step.py
@@ -0,0 +1,186 @@
+"""Build step class for stripping OTA test binaries."""
+
+# pylint triggers false positives in _setup/_teardown_mocks
+# pylint: disable=method-hidden
+
+from __future__ import absolute_import
+import logging
+import os
+import sys
+
+from slave import base_step
+
+_UNIT_TEST_ARCHIVE_MODULE = 'unit_test.unit_test_archive_lib'
+_UNIT_TEST_HELPER_MODULE = 'unit_test.unit_test_helper'
+
+# If a file is larger than this, the strip tool will fail to recognize the file.
+_MAX_32BIT_FILESIZE = 2**32
+_MAX_32BIT_FILESIZE_STR = '4GB'
+_STRIPPED_EXT = '.stripped'
+
+
+def _InjectPythonPath(path):
+  if path not in sys.path:
+    sys.path.append(path)
+
+
+class OtaStripBinariesStep(base_step.BaseStep):
+  """Build step class for stripping OTA test binaries."""
+
+  def __init__(self, **kwargs):
+    """Creates a OtaStripBinariesStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='strip binaries', **kwargs)
+    # Use the unit_test_helper modules to be consistent with Catabuilder.
+    self._unit_test_helper = None
+    self._unit_test_archive_lib = None
+    self._const = None
+    # Save list of files that need to be cleaned up at the end.
+    # Can't cleanup stripped binaries until after done archiving them.
+    self._stripped_binaries = []
+
+  @property
+  def const(self):
+    if not self._const:
+      self._const = self.unit_test_helper.GetUnitTestConst(None)
+    return self._const
+
+  @property
+  def gcs_archive_dir(self):
+    output_dir = os.path.join(self.get_gcs_dir(), 'artifacts', 'tests')
+    if not os.path.exists(output_dir):
+      os.makedirs(output_dir)
+    return output_dir
+
+  @property
+  def unit_test_helper(self):
+    """Use the unit_test_helper module to be consistent with Catabuilder."""
+    if not self._unit_test_helper:
+      _InjectPythonPath(self.get_project_path('test'))
+      self._unit_test_helper = __import__(
+          _UNIT_TEST_HELPER_MODULE, fromlist=[''])
+    return self._unit_test_helper
+
+  @property
+  def unit_test_archive_lib(self):
+    """Use unit_test_archive_lib module to be consistent with Catabuilder."""
+    if not self._unit_test_archive_lib:
+      _InjectPythonPath(self.get_project_path('test'))
+      self._unit_test_archive_lib = __import__(
+          _UNIT_TEST_ARCHIVE_MODULE, fromlist=[''])
+    return self._unit_test_archive_lib
+
+  def _get_out_dir(self):
+    chromium_src_dir = self.get_project_path('chromium/src')
+    return self.unit_test_helper.GetOutDir(chromium_src_dir)
+
+  def _get_binaries_to_strip(self, out_dir):
+    return [
+        binary for binary in
+        self.unit_test_helper.GetListOfBinariesToBeStripped(out_dir)
+        if binary.strip()
+    ]
+
+  def _clear_test_binaries(self, test_binary_paths):
+    for binary in test_binary_paths:
+      stripped_binary = binary + _STRIPPED_EXT
+      if os.path.exists(stripped_binary):
+        try:
+          os.remove(stripped_binary)
+        except OSError:
+          continue
+
+  def _get_test_files_to_archive(self, out_dir, stripped_binary_paths):
+    """Gets the set of test binaries & dependency files to archive."""
+    stripped_binary_names = set(
+        [os.path.basename(path) for path in stripped_binary_paths])
+    # Get a flattened deduped list of all binaries & dependencies to tar.
+    tar_files = set()
+    deps_dict = self.unit_test_archive_lib.GetAllTestDependenciesDict(
+        self.const, out_dir)
+    for test_deps in deps_dict.values():
+      for dependency in test_deps:
+        # Use the stripped version of test binaries where available.
+        if os.path.basename(
+            dependency) in stripped_binary_names and os.path.exists(
+                os.path.join(self.const.chrome_srcroot,
+                             dependency + _STRIPPED_EXT)):
+          tar_files.add(dependency + _STRIPPED_EXT)
+        else:
+          tar_files.add(dependency)
+    return tar_files
+
+  def _strip_binary(self, file_path, output_binary):
+    return self.unit_test_helper.StripBinary(
+        file_path, output_file=output_binary)
+
+  def _strip_error_to_comment(self, binary, stderr):
+    """Convert errors from the strip tool into comments for gerrit."""
+    if not stderr:
+      return ''
+
+    comment = stderr.replace(os.path.abspath(os.getcwd()), '')
+    binary_size = os.path.getsize(binary)
+    if binary_size > _MAX_32BIT_FILESIZE:
+      logging.warning('%s=%s bytes > %s', binary, binary_size,
+                      _MAX_32BIT_FILESIZE_STR)
+      comment += ('{binary} ({size:,}bytes) is larger than {max_size}.'
+                  ' Too large to strip. (b/33284348)'.format(
+                      binary=os.path.basename(binary),
+                      size=binary_size,
+                      max_size=_MAX_32BIT_FILESIZE_STR))
+    return comment
+
+  def _strip_binaries(self, test_binaries):
+    """Strip the test binaries built for the most recent ota."""
+    failure_messages = []
+    for binary in test_binaries:
+      # Try to strip the binary.
+      output_binary = binary + _STRIPPED_EXT
+      try:
+        self._strip_binary(binary, output_binary)
+      except IOError as e:
+        # If stripping failed, report the error.
+        failure_messages.append(self._strip_error_to_comment(binary, str(e)))
+    return failure_messages
+
+  def _archive_binaries(self, files_to_archive):
+    self.unit_test_archive_lib.ZipWithRelativeFileList(
+        self.const.chrome_srcroot,
+        os.path.join(self.gcs_archive_dir, 'test_deps.tar.gz'),
+        files_to_archive,
+        # Remove .stripped from all filenames within the tar.
+        transform=r's|{}$||'.format(_STRIPPED_EXT))
+
+  def run(self):
+    """Run the strip tool over the unittest binaries and archive them to GCS."""
+    try:
+      out_dir = self._get_out_dir()
+    except ValueError as error:
+      # |out_dir| may not exist if a previous build step failed. However, we
+      # still want this step to be marked as failing in case the out_dir does
+      # actually exist somewhere and this step isn't finding it correctly.
+      self.add_review({'message': error.message})
+      return False
+
+    test_binary_paths = self._get_binaries_to_strip(out_dir)
+    # Clear in case recipe aborted in a previous run before able to cleanup
+    self._clear_test_binaries(test_binary_paths)
+
+    failure_messages = self._strip_binaries(test_binary_paths)
+    if failure_messages:
+      self.add_review({'message': '\n'.join(failure_messages)})
+      return False
+
+    # Archive stripped test binaries into a tar file in gcs_archive_dir
+    # so that they'll be uploaded to GCS. Catatester will use the tar file
+    # for running these tests on-device in the lab.
+    files_to_archive = self._get_test_files_to_archive(out_dir,
+                                                       test_binary_paths)
+    self._archive_binaries(files_to_archive)
+
+    self._clear_test_binaries(test_binary_paths)
+    return True
diff --git a/cq/scripts/slave/step/partner_build_check_step.py b/cq/scripts/slave/step/partner_build_check_step.py
new file mode 100644
index 0000000..432e808
--- /dev/null
+++ b/cq/scripts/slave/step/partner_build_check_step.py
@@ -0,0 +1,67 @@
+"""Build step class for running partner build checks on source tree."""
+
+from __future__ import absolute_import
+import logging
+import os
+
+from slave import base_step
+
+_PARTNER_BUILD_ERROR_MSG = ('Your change causes cast_shell to depend on '
+                            'libcutil. This is not acceptable for partner '
+                            'builds. Please rework your CL!')
+
+_READELF_FAILURE_MSG = ('readelf returned failure. Please make sure cast_shell'
+                        'is built and readelf is installed!')
+
+
+class PartnerBuildCheckStep(base_step.BaseStep):
+  """Build step class for checking GN desc."""
+
+  def __init__(self, build_name, build_flavor, **kwargs):
+    """Creates a PartnerBuildCheckStep instance.
+
+    This step requires ota build to be completed since it checks on cast_shell
+    executable.
+
+    Args:
+      build_name: Name of build (e.g. pepperoni-eng, tvdefault-eng).
+      build_flavor: Flavor of build ('debug' or 'release').
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='partner build check', **kwargs)
+
+    self._product = build_name.split('-')[0]
+    self._build_flavor = build_flavor
+
+  def run(self):
+    """Runs gn desc on all changed GN files for the current commit.
+
+    Sets a build property "review" with the output.
+
+    Returns:
+      True iff there were no issues in partner build.
+    """
+    cast_shell_path = os.path.join('chromium', 'src',
+                                   'out_chromecast_' + self._product,
+                                   self._build_flavor,
+                                   'cast_shell')
+    if not os.path.isfile(cast_shell_path):
+      logging.info('cast_shell is not built, cannot check partner build')
+      self.add_review({'message': 'Ota build did not complete'})
+      return False
+
+    command = ['readelf',
+               '-d',
+               cast_shell_path]
+
+    returncode, stdout, _ = self.exec_subprocess(command)
+
+    if returncode != 0:
+      self.add_review({'message': _READELF_FAILURE_MSG})
+      return False
+
+    if 'libcutil' in stdout:
+      self.add_review({'message': _PARTNER_BUILD_ERROR_MSG})
+      return False
+
+    return True
diff --git a/cq/scripts/slave/step/platform_info_step.py b/cq/scripts/slave/step/platform_info_step.py
new file mode 100644
index 0000000..7e2bc1a
--- /dev/null
+++ b/cq/scripts/slave/step/platform_info_step.py
@@ -0,0 +1,28 @@
+"""Platform Statistics Gathering step."""
+
+from __future__ import absolute_import
+import logging
+
+from helpers import branch_utils
+from slave import base_step
+
+
+class PlatformInfoStep(base_step.BaseStep):
+  """A quick step class for gathering useful os/platform information."""
+
+  def __init__(self, **kwargs):
+    """Creates a PlatformInfoStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='Platform Information', **kwargs)
+
+  def run(self):
+    kernel_version_cmd = ['/bin/uname', '-v']
+    cpu_info_cmd = ['/usr/bin/lscpu']
+
+    self.exec_subprocess(kernel_version_cmd, check_output=True)
+    self.exec_subprocess(cpu_info_cmd, check_output=True)
+
+    return True
diff --git a/cq/scripts/slave/step/precommit_step.py b/cq/scripts/slave/step/precommit_step.py
new file mode 100644
index 0000000..819643e
--- /dev/null
+++ b/cq/scripts/slave/step/precommit_step.py
@@ -0,0 +1,163 @@
+"""A step to run pre-commit hooks.
+
+See documentation on the precommit recipe - this module
+contains the single step in that recipe."""
+from __future__ import absolute_import
+import logging
+import os
+import pprint
+import tempfile
+
+from helpers import git_utils
+from helpers import os_utils
+from slave import base_step
+
+
+class PreCommitStep(base_step.BaseStep):
+  """Step for running pre-commit hook checks."""
+
+  def __init__(self, config_file, repo_root_directory, show_output, **kwargs):
+    """
+    Args:
+      config_file: The pre-commit config file to use.
+      repo_root_directory: The root directory to the repo checkout.
+      show_output: True to make the output a comment on the CL.
+      **kwargs: Additional args passed to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name="precommit", **kwargs)
+    self._config_file = config_file
+    self._repo_root_directory = repo_root_directory
+    self._show_output = show_output
+
+  def run(self):
+    """Run the step.
+
+    Returns:
+      True if either:
+        1. There is a pre-commit config and there were no pre-commit failures.
+        2. There was no pre-commit config.
+    """
+    try:
+        directory = self.directory
+    except KeyError:
+        logging.info("Unable to get self.directory. Skipping pre-commit.")
+        return True
+
+    pre_commit_config = os.path.join(directory, self._config_file)
+    if not os.path.exists(pre_commit_config):
+      logging.info("No pre-commit config at %s (%s), skipping",
+          pre_commit_config, os.path.abspath(pre_commit_config))
+      return True
+
+    results = [
+      self._run_commit_message_hooks(),
+      self._run_pre_commit_hooks(),
+      self._run_push_hooks(),
+    ]
+    returncode = max(returncode for returncode, _, _ in results)
+    stdout = "\n".join(stdout for _, stdout, _ in results)
+    stderr = "\n".join(stderr for _, _, stderr in results)
+    if self._show_output:
+      try:
+        self.add_review({
+          "comments": {},
+          "message": "stdout: {}\n\nstderr: {}".format(stdout, stderr),
+        })
+      except UnicodeDecodeError:
+        self.add_review({
+          "comments": {},
+          "message": ("Failed to decode precommit output as unicode, "
+            "please check the builder log directly."),
+        })
+    else:
+      self.add_review({
+        "comments": {},
+        "message": ("This builder is non-blocking and its output can be "
+                    "ignored."),
+      })
+    if returncode:
+      return False
+    return True
+
+  def _get_env(self):
+    env = os.environ.copy()
+    env["EUREKAROOT"] = self._repo_root_directory
+    logging.info("Using PATH: %s",
+      pprint.pformat(env.get("PATH", "").split(":")))
+    try:
+      chromium_src = self.get_project_path("chromium/src")
+      buildtools_path = os.path.join(
+        self._repo_root_directory,
+        chromium_src,
+        "buildtools")
+      logging.info("Setting CHROMIUM_BUILDTOOLS_PATH to %s", buildtools_path)
+      env["CHROMIUM_BUILDTOOLS_PATH"] = buildtools_path
+    except KeyError:
+      logging.info("Unable to set CHROMIUM_BUILDTOOLS_PATH, "
+        "no chromium/src found.")
+    return env
+
+  def _precommit_binary(self) -> str:
+    """Get the path to pre-commit in our recipe's virtualenv."""
+    return os.path.join(self.get_step_data("virtual_env_root"),
+      "bin", "pre-commit")
+
+  def _run_commit_message_hooks(self):
+    """Run all the hooks for commit message checks.
+
+    Returns:
+      A tuple of returncode, stdout, stderr from pre-commit
+    """
+    env = self._get_env()
+    with os_utils.change_cwd(self.directory):
+      commit_message = git_utils.commit_message(self)
+      commit_message_path = self.write_temp_file(commit_message)
+      logging.debug("Using commit message tempfile %s", commit_message_path)
+      cmd = [
+        self._precommit_binary(),
+        "run",
+        "--config", self._config_file,
+        "--hook-stage", "commit-msg",
+        "--commit-msg-filename", commit_message_path,
+      ]
+      return self.exec_subprocess(cmd, env=env)
+
+  def _run_pre_commit_hooks(self):
+    """Run all the hooks of the 'pre-commit' type.
+
+    This is an unfortunate naming problem - 'pre-commit' the framework
+    shares a name with the 'pre-commit' git hook type, which are hooks
+    to run before committing. This function runs just those hooks, as
+    opposted to pre-push, commit-message, or other hook types.
+
+    Returns:
+      A tuple of returncode, stdout, stderr from pre-commit
+    """
+    env = self._get_env()
+    with os_utils.change_cwd(self.directory):
+      cmd = [
+        self._precommit_binary(),
+        "run",
+        "--config", self._config_file,
+        "--source", "HEAD^",
+        "--origin", "HEAD"
+      ]
+      return self.exec_subprocess(cmd, env=env)
+
+  def _run_push_hooks(self):
+    """Run all the hooks of the 'push' stage.
+
+    Returns:
+      A tuple of returncode, stdout, stderr from pre-commit
+    """
+    env = self._get_env()
+    with os_utils.change_cwd(self.directory):
+      cmd = [
+        self._precommit_binary(),
+        "run",
+        "--hook-stage", "push",
+        "--config", self._config_file,
+        "--source", "HEAD^",
+        "--origin", "HEAD"
+      ]
+      return self.exec_subprocess(cmd, env=env)
diff --git a/cq/scripts/slave/step/py_env_setup_step.py b/cq/scripts/slave/step/py_env_setup_step.py
new file mode 100644
index 0000000..01229bd
--- /dev/null
+++ b/cq/scripts/slave/step/py_env_setup_step.py
@@ -0,0 +1,80 @@
+"""Build step class for setting up a Python environment."""
+from pathlib import Path
+import os
+from typing import Iterable, Optional
+
+from helpers import py_env_utils
+from slave import base_step
+
+
+class PyEnvSetupStep(base_step.BaseStep):
+  """Step for setting up python envs."""
+
+  def __init__(self,
+               additional_import_paths: Optional[Iterable[Path]]=None,
+               local_package_project: str=None,
+               local_packages_dir: str=None,
+               py3: bool=False,
+               requirements_project: str=None,
+               requirements_path: str=None,
+               **kwargs):
+    """Creates a PyEnvSetupStep instance.
+
+    Args:
+      additional_import_paths: Paths to add to sys.path for the environment
+      local_package_project: manifest project to look a python packages on disk
+      local_packages_dir: Relative path to python packages under ~/gcs-mirror
+      py3: if True use python3
+      requirements_project: manifest project to look for requirements_path
+      requirements_path: path to requirements file under requirements_project
+      gcs_mirror_path: The directory that mirrors the gcs bucket
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    super().__init__(name='py_env_setup', **kwargs)
+    self._gcs_dir = self._properties.get(
+        'gcs_mirror_path', '/workspace/gcs-mirror')
+    self._additional_import_paths = additional_import_paths
+    self._local_package_project = local_package_project
+    self._local_packages_dir = local_packages_dir
+    self._py3 = py3
+    self._requirements_path = requirements_path
+    self._requirements_project = requirements_project
+
+  def run(self) -> bool:
+    """Sets up a Python environment.
+
+    Adds 'virtual_env_root' to the step data.
+
+    Returns:
+      True iff the environment was set up correctly.
+    """
+    py_requirements_dir = self.get_project_path(self._requirements_project) \
+        if self._requirements_project else None
+    local_package_path = self.get_project_path(self._local_package_project) \
+        if self._local_package_project else None
+    venv_root = py_env_utils.create_virtual_env(
+        self,
+        self.get_tmp_dir(),
+        gcs_dir=self._gcs_dir,
+        local_packages=self._local_packages_dir,
+        py3=self._py3)
+    py_env_utils.add_import_paths(venv_root, [Path.home() / 'depot_tools'])
+    self.add_step_data('virtual_env_root', venv_root)
+    if py_requirements_dir:
+      requirements = os.path.join(py_requirements_dir,
+                                  self._requirements_path)
+      py_env_utils.install_py_deps(
+          self, venv_root, requirements, self._gcs_dir, self._local_packages_dir
+      )
+    if local_package_path:
+      py_env_utils.install_package(
+          self,
+          venv_root,
+          local_package_path,
+          self._gcs_dir,
+          local_packages=self._local_packages_dir
+      )
+
+    if self._additional_import_paths:
+      py_env_utils.add_import_paths(venv_root, self._additional_import_paths)
+    return True
diff --git a/cq/scripts/slave/step/py_test_step.py b/cq/scripts/slave/step/py_test_step.py
new file mode 100644
index 0000000..0b82c99
--- /dev/null
+++ b/cq/scripts/slave/step/py_test_step.py
@@ -0,0 +1,95 @@
+"""Build step class for running Python tests."""
+import json
+import logging
+import os
+
+from helpers import py_env_utils
+from slave import base_step
+
+_UNITTEST_RESULT_MARKER = '__CQ_UNITTEST_RESULTS_SUMMARY__:'
+
+
+class PyTestStep(base_step.BaseStep):
+  """Step for running python tests."""
+
+  def __init__(self,
+               project,
+               test_file_filter_regex,
+               timeout,
+               module_path_regex='.',
+               **kwargs):
+    """Creates a PyTestStep instance.
+
+    Args:
+      project: Manifest project to look for tests inside of it.
+      test_file_filter_regex: File pattern for tests to be run.
+      timeout: Timeout for test run (in seconds).
+      module_path_regex: Regex to determine which modules to include.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='py_test', **kwargs)
+    self._project = project
+    self.test_file_filter_regex = test_file_filter_regex
+    self.module_path_regex = module_path_regex
+    self.timeout = timeout
+
+  def _execute_tests(self, venv_root):  # pylint: disable=method-hidden
+    """Execute the tests.
+
+    Args:
+      venv_root: Path to the python virtual env root to activate.
+
+    Returns:
+      Tuple: (review, passing) Where passing is a boolean if the tests
+      passed and review is the generated review message.
+    """
+    test_runner = os.path.abspath(
+        os.path.join(self.get_cq_root(),
+                     'scripts/helpers/py_test_runner.py'))
+    directory = os.path.join(
+        self._getcwd(), self.get_project_path(self._project))
+    test_results = os.path.join(self.get_tmp_dir(), 'test_review.json')
+    cmd = [test_runner,
+           '--test_dir', directory,
+           '--output', test_results,
+           '--test_file_filter_regex', self.test_file_filter_regex,
+           '--module_path_regex', self.module_path_regex,
+           '--timeout', str(self.timeout)]
+    env = {
+        'SHELL_EXE_BINARY_PATH': self.get_step_data('cast_shell_exe_path', ''),
+    }
+    returncode, stdout, stderr = py_env_utils.exec_within_virtual_env(
+        self, venv_root, cmd, env=env)
+
+    if returncode != 0:
+      logging.error('Failed to run tests: %s\n%s', stdout, stderr)
+      return {'message': 'Tests did not run properly', 'passing': False}
+
+    return self._parse_test_results(test_results)
+
+  def _parse_test_results(self, test_results_file):
+    with open(test_results_file) as f:
+      return json.load(f)
+
+  def run(self):
+    """Runs python tests on the repository.
+
+    Sets a build property "review" with the output.
+
+    Returns:
+      True iff there were no test errors.
+    """
+    venv_root = self.get_step_data('virtual_env_root')
+    try:
+      review = self._execute_tests(venv_root)
+      passing = review['passing']
+      if passing:
+        logging.info(review.get('message'))
+      else:
+        self.add_review(review)
+    except Exception as e:  # pylint: disable=broad-except
+      logging.exception('Failure while executing tests.')
+      self.add_review({'message': 'Failed due to exception [{}].'.format(e)})
+      return False
+
+    return passing
diff --git a/cq/scripts/slave/step/pylint_step.py b/cq/scripts/slave/step/pylint_step.py
new file mode 100644
index 0000000..26217c1
--- /dev/null
+++ b/cq/scripts/slave/step/pylint_step.py
@@ -0,0 +1,63 @@
+"""Build step class for linting Python files with pylint."""
+
+from __future__ import absolute_import
+import logging
+import os
+
+from helpers import git_utils
+from helpers import pylint
+from slave import base_step
+
+PYLINTRC_NAMES = ['pylintrc', '.pylintrc']
+
+class PylintStep(base_step.BaseStep):
+  """Build step class for linting Python files."""
+
+  def __init__(self, pylintrc_file=None, **kwargs):
+    """Creates a PylintStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='pylint', **kwargs)
+    self._pylintrc_file = pylintrc_file
+
+  def run(self):
+    """Runs pylint on all changed files for the current commit.
+
+    Sets a build property "review" with the output.
+
+    Returns:
+      True iff there were no lint errors.
+    """
+    changed_files = git_utils.changed_files(
+        self, directory=self.directory, file_extensions=['.py'],
+        file_exclusion_pattern=r'^.+_pb2.py$')
+
+    changed_files = [f
+                     for f in changed_files
+                     if self.allow_listed(f) and not self.deny_listed(f)]
+
+    if not self._pylintrc_file:
+      for name in PYLINTRC_NAMES:
+        path = os.path.join(self.directory, name)
+        if os.path.isfile(path):
+          self._pylintrc_file = path
+          break
+    try:
+      lint_output = pylint.RunPylint(
+          changed_files, pylintrc_file=self._pylintrc_file, cwd=self.directory,
+          executor=self)
+      comments = pylint.ParseLint(lint_output.splitlines())
+
+      if not comments:
+        logging.info('Autogenerated lint comments: looks clean')
+        return True
+    except pylint.PylintError as e:
+      message = str(e)
+      self.add_review({'message': message})
+      return False
+
+    self.add_review({'message': 'Autogenerated lint comments',
+                     'comments': comments})
+    return False
diff --git a/cq/scripts/slave/step/reproxy_start_step.py b/cq/scripts/slave/step/reproxy_start_step.py
new file mode 100644
index 0000000..7cbb4a0
--- /dev/null
+++ b/cq/scripts/slave/step/reproxy_start_step.py
@@ -0,0 +1,43 @@
+"""Build step class for starting reproxy."""
+
+from __future__ import absolute_import
+import logging
+
+from helpers import rbe_utils
+from slave import base_step
+
+
+class ReproxyStartStep(base_step.BaseStep):
+  """Build step class for starting the remote execution reproxy."""
+
+  def __init__(self, **kwargs):
+    """Creates a ReproxyStartStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    super().__init__(name='reproxy start', **kwargs)
+
+  def run(self):
+    """Starts the reproxy process.
+
+    Starts reproxy via the bootstrap control binary.
+
+    Returns:
+      True if reproxy has started successfully.
+    """
+
+    if not rbe_utils.is_installed():
+      if self.build_system == 'catabuilder':
+        return True
+      logging.warning(
+          'RBE was requested (via use_rbe=True) but it is not installed on '
+          'this branch. go/cherry-pick-rbe to fix this build error.')
+      return False
+
+    retcode, _, _ = rbe_utils.start_reproxy(self)
+    if retcode != 0:
+      logging.error('reproxy failed to start')
+      return False
+
+    return True
diff --git a/cq/scripts/slave/step/reproxy_stop_step.py b/cq/scripts/slave/step/reproxy_stop_step.py
new file mode 100644
index 0000000..7740d32
--- /dev/null
+++ b/cq/scripts/slave/step/reproxy_stop_step.py
@@ -0,0 +1,38 @@
+"""Build step class for stopping reproxy."""
+
+from __future__ import absolute_import
+import logging
+
+from helpers import rbe_utils
+from slave import base_step
+
+
+class ReproxyStopStep(base_step.BaseStep):
+  """Build step class for stoping the remote execution reproxy."""
+
+  def __init__(self, **kwargs):
+    """Creates a ReproxyStopStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    super().__init__(name='reproxy stop', **kwargs)
+
+  def run(self):
+    """Stops the reproxy process.
+
+    Stops reproxy via the bootstrap control binary.  This also generates
+    the rbe_metrics.txt file.
+
+    Returns:
+      True always.  Don't fail the build if reproxy fails to stop.
+    """
+
+    if not rbe_utils.is_installed():
+      return True
+
+    retcode, _, _ = rbe_utils.stop_reproxy(self)
+    if retcode != 0:
+      logging.error('reproxy failed to stop')
+
+    return True
diff --git a/cq/scripts/slave/step/sanitizer_unittest_step.py b/cq/scripts/slave/step/sanitizer_unittest_step.py
new file mode 100644
index 0000000..c047a6a
--- /dev/null
+++ b/cq/scripts/slave/step/sanitizer_unittest_step.py
@@ -0,0 +1,80 @@
+"""Run a unittest with a sanitizer.
+
+The key feature of this step is that this step will only be marked as failed
+if the sanitizer detects an issue. If a test fails for another reason, then
+this step is not considered to have failed.
+
+Available sanitizers are: asan, msan, tsan, and ubsan.
+"""
+
+from __future__ import absolute_import
+import os
+
+from helpers import sanitizer_utils
+from slave.step import shell_step
+
+
+class SanitizerUnitTestShellStep(shell_step.ShellStep):
+  """Unit test step running with a sanitizer ."""
+
+  def __init__(self, sanitizer=None, **kwargs):
+    """Constructor."""
+    assert sanitizer in ['asan', 'msan', 'tsan', 'ubsan']
+    properties_handler = sanitizer_utils.get_sanitizer_to_comments_for(
+        sanitizer)
+    shell_step.ShellStep.__init__(
+        self, properties_handler=properties_handler, **kwargs)
+    self._sanitizer = sanitizer
+    self._unittest_prefix_filter = 'cast_'
+    if 'unittest_prefix_filter' in kwargs:
+      self._unittest_prefix_filter = kwargs['unittest_prefix_filter']
+
+
+    symbolizer = os.path.relpath(
+        os.path.join(
+            self.get_project_path('chromium/src'),
+            'third_party/llvm-build/Release+Asserts/bin/llvm-symbolizer'),
+        self.directory)
+    suppression_dir = os.path.relpath(
+        os.path.join(
+            self.get_project_path('chromecast/internal'), 'build/sanitizers'),
+        self.directory)
+    asan_options = ('detect_leaks=1 '
+                    'detect_odr_violation=0 '
+                    'symbolize=1 '
+                    'external_symbolizer_path={}'.format(symbolizer))
+    lsan_options = ('suppressions={}/lsan_suppressions'.format(suppression_dir))
+    tsan_options = ('external_symbolizer_path={} '
+                    'suppressions={}/tsan_suppressions').format(symbolizer,
+                                                                suppression_dir)
+    ubsan_options = ('external_symbolizer_path={}'.format(symbolizer))
+
+    self._sanitizer_env = {
+        'ASAN_OPTIONS': asan_options,
+        'LSAN_OPTIONS': lsan_options,
+        'MSAN_SYMBOLIZER_PATH': symbolizer,
+        'TSAN_OPTIONS': tsan_options,
+        'UBSAN_OPTIONS': ubsan_options,
+    }
+
+  @property
+  def validator(self):
+    # Non-cast unittests (i.e. upstream Chromium tests) are not MSAN clean, so
+    # do not block on them until upstream MSAN bots are stabilized.
+    # (crbug/658452)
+    if not self._name.startswith(self._unittest_prefix_filter):
+      return lambda *args: True
+    return sanitizer_utils.get_validator_for(self._sanitizer)
+
+  def get_commands(self):
+    """Returns which commands to be run. Can be overridden by a subclass.
+
+    Returns:
+      List of ShellCommand objects to execute.
+    """
+    assert isinstance(self._command, list)
+    return [self.ShellCommand(
+        self._command,
+        validator=self.validator,
+        cwd=self.directory,
+        env=self._sanitizer_env)]
diff --git a/cq/scripts/slave/step/shell_step.py b/cq/scripts/slave/step/shell_step.py
new file mode 100644
index 0000000..a1f47c8
--- /dev/null
+++ b/cq/scripts/slave/step/shell_step.py
@@ -0,0 +1,230 @@
+"""Build step class for executing a simple shell command."""
+
+from __future__ import absolute_import
+import logging
+import time
+
+import six
+from helpers import retry_utils
+from helpers import signal_utils
+from slave import base_step
+
+class ShellStep(base_step.BaseStep):
+  """Build step class for executing a shell command."""
+
+  class ShellCommand(object):
+    """A simple object wrapping a shell command to execute."""
+
+    def __init__(self, command, validator=None, cwd=None, env=None,
+                 num_retries=0, setup_for_retry=None, halt_on_failure=False,
+                 retry_if=None):
+      """Constructor for a ShellCommand object.
+
+      Args:
+        command: The command to be run, as a list of strings (required).
+        validator: A function that takes 3 arguments: returncode, stdout, stderr
+            from the run command and returns a boolean value indicating if the
+            command succeeded (True) or failed (False).
+            (Default checks if returncode == 0)
+        cwd:     The directory from which this command is to be run. This will
+                 override the current directory of the step.
+        env:     A dictionary mapping environment variables to their values.
+        num_retries: The number of times to retry this step if it fails.
+            (Defaults to 0)
+      retry_if: A function that takes 3 arguments: returncode, stdout, stderr
+          from the run command and returns whether to retry the command (True)
+          or to skip retrying (False).
+        setup_for_retry: A function to run before attempting to retry this
+            command. Can be used to do work that may help the command
+            succeed upon retry. Only used if num_retries > 0.
+        halt_on_failure: Boolean if we should stop running subsequent steps on
+            failure. (Defaults to False)
+      """
+      assert isinstance(command, list)
+      assert not cwd or isinstance(cwd, six.string_types)
+      assert not env or isinstance(env, dict)
+
+      self.command = command
+      self._validator = validator
+      self.cwd = cwd
+      self.env = env or {}
+      self._num_retries = num_retries
+      self._retry_if = retry_if
+      self._setup_for_retry = setup_for_retry
+      self._halt_on_failure = halt_on_failure
+
+    @property
+    def num_retries(self):
+      """Number of retries to run."""
+      return self._num_retries
+
+    @property
+    def halt_on_failure(self):
+      """Boolean if we should halt on a failure."""
+      return self._halt_on_failure
+
+    def exec_retry_setup(self, returncode, stdout, stderr):
+      """Run this before attempting to retry this command.
+
+      If command has failed from a recoverable issue, then this can be used
+      to recover so that the next retry attempt of |self.command| will succeed.
+
+      Args:
+        returncode: returncode of |self.command| that failed and is about to
+            be retried.
+        stdout: stdout of |self.command| that failed and is about to be retried.
+        stderr: stderr of |self.command| that failed and is about to be retried.
+      """
+      if self._setup_for_retry:
+        self._setup_for_retry(returncode, stdout, stderr)
+
+    def validate(self, returncode, stdout, stderr):
+      if self._validator:
+        return self._validator(returncode, stdout, stderr)
+      return returncode == 0
+
+    def retry_if(self, returncode, stdout, stderr):
+      """If command fails validator, check if it should be retried."""
+      if self._retry_if:
+        return self._retry_if(returncode, stdout, stderr)
+      return returncode != 0
+
+  def __init__(self, name=None, command=None, validator=None,
+               properties_handler=None, force_success=False, num_retries=0,
+               retry_if=None, cwd=None, env=None, **kwargs):
+    """Creates a ShellStep instance.
+
+    Args:
+      name: name of this step
+      command: shell command to run, as a list (argv).
+      validator: A function that takes 3 arguments: returncode, stdout, stderr
+          from the run command and returns a boolean value indicating if the
+          command succeeded (True) or failed (False).
+          (Default checks if returncode == 0)
+      properties_handler: if provided, method for providing properties from the
+                          command output.
+      force_success: Force this step to return success, even if the shell
+          command returns non-zero. (Defaults to False)
+      num_retries: The number of times to retry this step if it fails.
+          (Defaults to 0)
+      retry_if: A function that takes 3 arguments: returncode, stdout, stderr
+          from the run command and returns whether to retry the command (True)
+          or to skip retrying (False).
+      cwd: The directory from which the ShellCommands will be run if cwd is
+          not set on the ShellCommand. This will override the current directory
+          for the step also.
+      env: A dictionary mapping environment variables to their values to be
+          used if no env is set on the ShelLCommands.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name=name, **kwargs)
+
+    self._command = command
+    self._validator = validator
+    self._properties_handler = properties_handler
+    self._force_success = force_success
+    self._num_retries = num_retries
+    self._retry_if = retry_if
+    self._cwd = cwd
+    self._env = env
+
+  def _execute_cmd(self, cmd):
+    """Executes a ShellCommand.
+
+    Args:
+      cmd: A ShellCommand instance.
+
+    Returns:
+      A tuple of:
+        Boolean if the command was successful, returncode, stdout, stderr
+    """
+    directory = cmd.cwd or self._cwd or self.directory or None
+    environment = cmd.env or self._env or {}
+    retry = True
+    attempt = 0
+    success = True
+    while retry:
+      returncode, stdout, stderr = self.exec_subprocess(
+          cmd.command, cwd=directory, env=environment)
+      success = cmd.validate(returncode, stdout, stderr)
+      retry = attempt < cmd.num_retries and not success
+      if retry and cmd.retry_if(returncode, stdout, stderr):
+        delay = retry_utils.retry_delay(attempt)
+        attempt += 1
+        logging.info(
+            'Retrying command in %s seconds. Attempt %s of %s.',
+            delay, attempt, cmd.num_retries)
+        time.sleep(delay)
+        cmd.exec_retry_setup(returncode, stdout, stderr)
+    return success, returncode, stdout, stderr
+
+  def run(self):
+    """Executes this step by running all commands found in get_commands().
+
+    Returns:
+      True iff the return code of every command was 0.
+    """
+    retry, success = True, True
+    returncode, stdout, stderr = 0, '', ''
+    attempt = 0
+
+    while retry:
+      try:
+        success = True  # Reset success for each retry
+        for shell_command in self.get_commands():
+          cmd_success, returncode, stdout, stderr = self._execute_cmd(
+              shell_command
+          )
+
+          success = self._force_success or cmd_success
+          if shell_command.halt_on_failure and not success:
+            break
+      except signal_utils.SigtermException:
+        # We want to skip retries in case of a Sigterm because this means we
+        # got a signal from outside to stop current execution.
+        raise
+      except Exception:  # pylint: disable=broad-except
+        # Absorb all exceptions until there are no retries left.
+        if attempt < self._num_retries:
+          logging.exception(('Step exception during [%s] ignored because there '
+                             'are still retries(%d) remainining.'),
+                            self.get_name(),
+                            self._num_retries - attempt)
+          success = False
+        else:
+          logging.exception('Step exception during [%s] with no retries left.',
+                            self.get_name(),)
+          return False
+
+      finally:
+        retry = attempt < self._num_retries and not success
+
+      if retry:
+        delay = retry_utils.retry_delay(attempt)
+        attempt += 1
+        logging.info(
+            'Retrying [%s] in %s seconds. Attempt %s of %s.',
+            self.get_name(), delay, attempt, self._num_retries)
+        time.sleep(delay)
+
+    # Only set properties from the final run, otherwise properties from
+    # failed runs and successful runs could be mixed together.
+    if self._properties_handler:
+      new_properties = self._properties_handler(returncode, stdout, stderr)
+      for property_name in new_properties:
+        if property_name == 'review':
+          self.add_review(new_properties[property_name])
+        else:
+          self.set_build_property(property_name, new_properties[property_name])
+
+    return success
+
+  def get_commands(self):
+    """Returns which commands to be run. Can be overridden by a subclass.
+
+    Returns:
+      List of ShellCommand objects to execute.
+    """
+    assert isinstance(self._command, list)
+    return [self.ShellCommand(self._command, validator=self._validator,
+                              retry_if=self._retry_if)]
diff --git a/cq/scripts/slave/step/sprockets_step.py b/cq/scripts/slave/step/sprockets_step.py
new file mode 100644
index 0000000..3a1d878
--- /dev/null
+++ b/cq/scripts/slave/step/sprockets_step.py
@@ -0,0 +1,88 @@
+"""Step for running Sprockets protocol conformance tests.
+
+See go/sprockets-eng and go/stl-manual for more information.
+"""
+
+from __future__ import absolute_import
+import collections
+import json
+import logging
+import os
+
+from helpers import cast_shell_utils
+from slave import base_step
+
+_SprocketsTestConfig = collections.namedtuple('_SprocketsTestConfig',
+                                              'project, config')
+_SPROCKETS_PROJECT = 'google/sprockets'
+_SPROCKETS_TEST_DRIVER = 'test_driver.py'
+_SPROCKETS_TEST_CONFIGS = [
+    _SprocketsTestConfig(project='test', config='sprockets_tests/tests.json')
+]
+
+_CAST_SHELL_FAILURE_MSG = 'Failed to start up and communicate with cast_shell.'
+_SPROCKETS_TEST_FAILURE_MSG = (
+    'Sprockets test failed. See the build log for details.')
+
+
+class SprocketsConformanceTestStep(base_step.BaseStep):
+  """Step to run Sprockets tests."""
+
+  def __init__(self, **kwargs):
+    """Constructor."""
+    base_step.BaseStep.__init__(
+        self, name='sprockets conformance tests', directory='', **kwargs)
+
+  def read_config(self, config_file_path):
+    if not os.path.exists(config_file_path):
+      logging.info('%s does not exist. Skipping.', config_file_path)
+      return []
+    with open(config_file_path) as config_json:
+      return json.load(config_json)
+
+  def run(self):
+    """Returns which commands to be run. Can be overridden by a subclass.
+
+    Returns:
+      List of ShellCommand objects to execute.
+    """
+    try:
+      _, log_descriptor, log_path = cast_shell_utils.start_cast_shell(
+          self,
+          self.get_project_path('test'),
+          self.get_step_data(cast_shell_utils.CAST_SHELL_EXE_PATH),
+          self.get_tmp_dir(),
+          use_headless_ozone_platform=True,
+          enable_ccs_transport_v2=True)
+    except (cast_shell_utils.CastShellError,
+            cast_shell_utils.CastShellTimeoutError):
+      logging.exception('Failed to start cast_shell')
+      self.add_review({'message': _CAST_SHELL_FAILURE_MSG})
+      return False
+
+    sprockets_path = self.get_project_path(_SPROCKETS_PROJECT)
+    sprockets_test_runner = os.path.join(sprockets_path, _SPROCKETS_TEST_DRIVER)
+
+    success = True
+    for config in _SPROCKETS_TEST_CONFIGS:
+      config_path_root = self.get_project_path(config.project)
+      config_file_path = os.path.join(config_path_root, config.config)
+      for test in self.read_config(config_file_path):
+        test_path_root = self.get_project_path(test['project'])
+        test_manifest = os.path.join(test_path_root, test['manifest'])
+        test_command = ['python3', sprockets_test_runner, test_manifest]
+        test_command.extend(test['args'])
+        returncode, _, _ = self.exec_subprocess(test_command)
+        success = (returncode == 0) and success
+
+    if log_descriptor and log_path:
+      cast_shell_utils.upload_cast_shell_logs(log_descriptor, log_path,
+                                              self.get_gcs_dir(),
+                                              'sprockets_conformance_tests',
+                                              'cast_shell.logcat')
+    if not success:
+      self.add_review({
+          'message': _SPROCKETS_TEST_FAILURE_MSG
+      })
+
+    return success
diff --git a/cq/scripts/slave/step/unittest_step.py b/cq/scripts/slave/step/unittest_step.py
new file mode 100644
index 0000000..d7bc89c
--- /dev/null
+++ b/cq/scripts/slave/step/unittest_step.py
@@ -0,0 +1,627 @@
+"""Build step classes for building and running unit tests."""
+import logging
+import os
+import re
+
+import six
+
+from helpers import branch_utils
+from helpers import git_utils
+from helpers import gn_utils
+from helpers import ninja_utils
+from slave import base_step
+from slave.step import cast_shell_step
+from slave.step import sanitizer_unittest_step
+from slave.step import shell_step
+
+_BUILD_CHROMECAST_SCRIPT = 'build/build_chromecast.py'
+
+RUN_TEST_LIST_PATH = 'tests/run_test_list.txt'
+RUN_JUNIT_TEST_LIST_PATH = 'junit/run_junit_test_list.txt'
+
+STEP_DATA_TEST_LIST = 'step_data_test_list'
+STEP_DATA_JUNIT_TEST_LIST = 'step_data_junit_test_list'
+
+ANDROID_TEST_RUNNER_PROJECT = 'test'
+ANDROID_TEST_RUNNER_PATH = 'unit_test/atv_unit_test_runner.py'
+TRANSFORM_XML_TO_JSON_PATH = 'unit_test/transform_xml_results_to_json.py'
+
+UNIT_TEST_REPORT_PATH = 'unit_test_results'
+
+# This is the maximum number of modified files a single change can have
+# before we assume that we need to run all unit tests. This value exists
+# because it takes time to calculate which tests need to be run for a set
+# of files and for very large changes, like chromium merges, it's just better
+# to assume we need to run all tests.
+MAX_TARGETS_FOR_ALL_TESTS = 1000
+
+# There are some files that GN does not know about that could affect the
+# unit tests. If any of these files are touched, then build and run all the
+# unit tests in order to be safe.
+# A map of <project-name> -> <list of file regexes to match>.
+FORCE_RUN_ALL_TESTS_WHITELIST = {
+    'chromecast/internal': [r'build/.*', r'vendor/google3/prebuilts.json'],
+    'vendor/amlogic': [r'google3_prebuilts.json'],
+    'vendor/marvell': [r'google3_prebuilts.json'],
+}
+
+
+class GnExecutionError(RuntimeError):
+  """Exception raised when a gn command returns a non-zero return code."""
+
+
+class BuildAffectedUnitTestsStep(cast_shell_step.NonOtaBuildBaseStep):
+  """Build step class for building unit tests."""
+
+  def __init__(self,
+               branding,
+               build_type,
+               product,
+               build_flavor,
+               enable_assistant,
+               sanitizer=None,
+               code_coverage=False,
+               absolute_coverage=False,
+               build_args_product=None,
+               build_args_official=None,
+               junit=False,
+               **kwargs):
+    """Instantiate the class.
+
+    Args:
+      branding: Build branding (chromium or chrome).
+      build_type: Type of system to build for ('x86', 'clang', or 'arm').
+      product: product to build for (ie. 'chromecast', 'audio').
+      build_flavor: Flavor of build ('Debug', 'Eng', 'Release').
+      enable_assistant: enable Assistant functionality. Audio products only.
+      sanitizer: Clang saniziter to run with ('asan', 'msan', 'tsan', 'ubsan').
+      code_coverage: True if code coverage is enabled for this build.
+      absolute_coverage: True if you want to run coverage for all unit tests.
+      build_args_product: The name of one of the
+        chromecast/internal/build/args/product files to use for the GN args.
+      build_args_official: Whether the build is an official build. MUST be set
+        for any build that will ship to users. Disables debugging; do not set
+        for development builds.
+      junit: If True, build junit tests. Otherwise, build gtests (Default False)
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    if junit:
+      step_name = 'build affected junit tests'
+    else:
+      step_name = 'build affected gtest unit tests'
+    cast_shell_step.NonOtaBuildBaseStep.__init__(
+        self,
+        step_name,
+        branding,
+        build_type,
+        product,
+        build_flavor,
+        sanitizer=sanitizer,
+        build_args_product=build_args_product,
+        build_args_official=build_args_official,
+        **kwargs)
+    self._enable_assistant = enable_assistant
+    self._code_coverage = code_coverage
+    self._absolute_coverage = absolute_coverage
+    self._junit = junit
+    self._run_only_affected_tests = (
+        self.get_property('build_system') != 'catabuilder' and
+        branch_utils.is_branch_equal_to_or_later_than(self.manifest_branch,
+                                                      '1.27'))
+    self._changed_files = None
+
+  def BuildUnitTests(self):
+    archive_dir = self.get_gcs_dir()
+
+    build_script = os.path.join(
+        self.get_project_path('chromecast/internal'), _BUILD_CHROMECAST_SCRIPT)
+    command = [
+        'python3', build_script, '--archive_dir', archive_dir,
+        '--build_args_product', self._build_args_product, '--build_number',
+        self.build_number, '--chrome_root',
+        self.get_project_path('chromium/src')
+    ]
+    command += self.build_accelerator.build_chromecast_flags
+
+    # TODO(b/73231972) remove unused flags once migrated to new builds
+    if self._build_flavor:
+      command += ['--build_flavor', self._build_flavor]
+    if self._build_type:
+      command += ['--build_type', self._build_type]
+    if self._branding:
+      command += ['--chromecast_branding', self._branding]
+    if self._product:
+      command += ['--product_name', self._product]
+    if self._build_args_official:
+      command += ['--official']
+
+    command += ['--tests_only']
+
+    if self.get_property('build_system') == 'catabuilder':
+      command += ['--enable_gn_check']
+
+    if self._enable_assistant:
+      command += ['--enable_assistant']
+
+    if self._sanitizer:
+      command += ['--sanitizer', self._sanitizer]
+
+    if self._junit:
+      command += ['--use_junit']
+
+    if self._code_coverage:
+      command += ['--enable_code_coverage']
+
+    if not self._must_run_all_tests():
+      # Run `gn gen` to make sure the out folder exists before `gn refs` is run.
+      returncode, _, _ = self.exec_subprocess(command + ['--gn_gen_only'])
+      if returncode != 0:
+        return returncode
+
+      # Determine which tests are affected and only build those tests.
+      affected_tests = self._get_affected_tests()
+      if affected_tests:
+        logging.info('Building %s tests affected by this change.',
+                     len(affected_tests))
+        affected_tests_arg = ','.join(sorted(affected_tests))
+        command += ['--whitelisted_tests', affected_tests_arg]
+      else:
+        logging.info(
+            'Affected test calculation returned no results, will build all '
+            'tests instead.')
+
+    returncode, stdout, _ = self.exec_subprocess(command)
+
+    if returncode != 0:
+      comments = ninja_utils.ninja_failure_to_comments(stdout, max_errors=3)
+      for comment in comments:
+        self.add_review({'message': comment})
+    else:
+      returncode = self.ArchiveUnitTests()
+
+    return returncode
+
+  def _get_unit_test_out_dir(self):
+    """Generate build-specific out dir name.
+
+      Out dir created by _BUILD_CHROMECAST_SCRIPT in format below:
+      out_<build_type>_<product>_<gn>[_<sanitizer>]
+      e.g. out_arm_android_gn for content_shell_udon_arm_debug_gn build.
+
+    Returns:
+      Path to UnittestStep out dir
+    """
+    out_dir = ['out', self.build_type_dir, self.build_product_dir, 'gn']
+    if self._sanitizer:
+      out_dir.append(self._sanitizer)
+    return '_'.join(out_dir)
+
+  def ArchiveUnitTests(self, dest_name='test_deps.zip'):  # pylint: disable=method-hidden
+    """Saving build_tests_android.zip to gcs_dir.
+
+    Args:
+      dest_name: desired archive name after upload.
+
+    Returns:
+      0 if succeed, returncode > 0 if failed
+    """
+    # Skip archive step if no build_tests_android.zip expected
+    # build_tests_android.zip been generated by GetTestDepsTargets in
+    # chromecast/internal/build/build_chromecast.py
+    is_android_or_atv = any(product in self._build_args_product
+                            for product in ['cube', 'soba', 'udon', 'atv'])
+    if not is_android_or_atv or self._junit:
+      return 0
+    out_dir = self._get_unit_test_out_dir()
+
+    archive_path = 'chromium/src/{}/{}/tests/build_tests_android.zip'.format(
+        out_dir, self.build_flavor_dir)
+
+    # Archive only if build_tests_android.zip was generated
+    if not os.path.isfile(archive_path):
+      return 0
+
+    archive_dir = os.path.join(self.get_gcs_dir(), 'artifacts/tests')
+    command = ['mkdir', '-p', archive_dir]
+    returncode, _, _ = self.exec_subprocess(command)
+    if not returncode:
+      command = ['cp', archive_path, os.path.join(archive_dir, dest_name)]
+      returncode, _, _ = self.exec_subprocess(command)
+
+    return returncode
+
+  def _get_changed_files(self):
+    if self._changed_files is None:
+      all_patches = [{'project': self.patch_project}] + self.depends_on_list
+      self._changed_files = git_utils.get_changed_files_in_all_patches(
+          self, all_patches, self.get_project_path_lookup_table())
+    return self._changed_files
+
+  def _must_run_all_tests(self):
+    """Returns if all the unittests must be ran based on the changed files."""
+
+    if self._absolute_coverage:
+      return True
+
+    if not self._run_only_affected_tests:
+      return True
+
+    for project, patterns in FORCE_RUN_ALL_TESTS_WHITELIST.items():
+      for pattern in patterns:
+        full_pattern = os.path.join(self.get_project_path(project), pattern)
+        if any(re.search(full_pattern, f) for f in self._get_changed_files()):
+          return True
+
+    # .gni files can get imported anywhere and cause deps, configs, etc.
+    # to change. It would be too burdensome to try to find the affected
+    # targets in this case, so we need to build/run all the tests.
+    if any(f.endswith('.gni') for f in self._get_changed_files()):
+      return True
+
+    return False
+
+  def _get_affected_tests(self):
+    """Return a list of the testing binaries affected by the changed files.
+
+    Uses gn to determine which test binaries are affected.
+
+    Returns:
+      A set of testing binaries affected by the changed files.
+
+    Raises:
+      GnUtilsError: If the gn binary cannot be found.
+      GnExecutionError: If the gn command fails.
+    """
+
+    if not self._get_changed_files():
+      return set()
+
+    # Non-gn files can be passed to gn refs normally. If a CL modifies a
+    # BUILD.gn file, then instead pass a GN label that matches against
+    # all targets in that BUILD.gn file, otherwise GN will effectively
+    # ignore the file. (e.g. //foo/bar/BUILD.gn -> //foo/bar:*).
+    gn_refs_targets = []
+    for f in self._get_changed_files():
+      # Only care about the BUILD.gn files in chromium/src. Some projects
+      # outside chromium/src also contain BUILD.gn files,
+      # e.g. <eurekaroot>/external/gwifi/libchrome-cros, which should
+      # NOT be considered to affect unit tests
+      if f.endswith('BUILD.gn') and f.startswith('chromium/src'):
+        gn_refs_targets.append(gn_utils.filepath_to_gn_path(f))
+      else:
+        gn_refs_targets.append(f)
+
+    gn_binary = gn_utils.get_gn_binary(self)
+
+    is_android_or_atv = any(product in self._build_args_product
+                            for product in ['cube', 'soba', 'udon', 'atv'])
+    if is_android_or_atv:
+      return self._get_android_affected_tests(gn_binary)
+
+    return self._get_linux_affected_tests(gn_binary, gn_refs_targets)
+
+  def _get_linux_affected_tests(self, gn_binary, targets):
+    # If there's just way too many targets then just use all unit tests
+    # otherwise we could be waiting a very long time calculating that we
+    # do, in fact, need to run all the unit tests
+    if len(targets) > MAX_TARGETS_FOR_ALL_TESTS:
+      command = [
+          gn_binary, 'refs',
+          '--root={}'.format(self.get_project_path('chromium/src')),
+          self._outdir, '--testonly=true', '--as=output', '--type=executable',
+          '--all', '-q', '*'
+      ]
+    else:
+      # The non-android test templates generate normal test executables, so
+      # look for those.
+      targets_str = '\n'.join(targets + [''])
+      tmp_file_path = self.write_temp_file(targets_str)
+
+      command = [
+          gn_binary, 'refs',
+          '--root={}'.format(self.get_project_path('chromium/src')),
+          self._outdir, '--testonly=true', '--as=output', '--type=executable',
+          '--all', '-q', '@' + tmp_file_path
+      ]
+
+    returncode, stdout, _ = self.exec_subprocess(command)
+
+    if returncode != 0:
+      raise GnExecutionError(
+          'Could not determine the tests affected by this change.')
+
+    return set(test.strip() for test in stdout.splitlines() if test.strip())
+
+  def _get_android_affected_tests(self, gn_binary):
+
+    # Determining affected tests for Android builds has been non-deterministic.
+    # (b/145934413, b/132997084, etc.) allowing breaking changes through
+    # presubmit. Until that is fixed, always run all test suites.
+    command = [
+        gn_binary, 'refs',
+        '--root={}'.format(self.get_project_path('chromium/src')), self._outdir,
+        '--testonly=true', '--as=label',
+        '--type={}'.format('group' if self._junit else 'action'), '--all', '-q',
+        '*'
+    ]
+
+    returncode, stdout, _ = self.exec_subprocess(command)
+
+    if returncode != 0:
+      raise GnExecutionError(
+          'Could not determine the tests affected by this change.')
+
+    # Convert each label returned in |stdout| to a GN target name. Some of the
+    # labels are legitimate test targets, but many are intermediate targets
+    # which are internal details of GN templates. Do some basic filtering to
+    # cut the list down. The rest will be filtered by the cast_test_list.
+    return set(
+        gn_utils.gn_label_to_target_name(label.strip())
+        for label in stdout.splitlines()
+        if label.strip() and '__' not in label)
+
+  def run(self):
+    """Builds unittests.
+
+    Returns:
+      True iff there were no errors.
+    """
+    returncode = self.BuildUnitTests()
+
+    if returncode != 0:
+      return False
+
+    run_test_list = (
+        RUN_JUNIT_TEST_LIST_PATH if self._junit else RUN_TEST_LIST_PATH)
+    step_data_key = (
+        STEP_DATA_JUNIT_TEST_LIST if self._junit else STEP_DATA_TEST_LIST)
+
+    # Load the tests to be run.
+    test_list = self.exec_read_file(os.path.join(self._outdir,
+                                                 run_test_list)).splitlines()
+
+    # Load the tests affected by this set of patches.
+    try:
+      affected_tests = self._get_affected_tests()
+    except gn_utils.GnUtilsError as e:
+      self.add_review({'message': str(e)})
+      return False
+    except RuntimeError as e:
+      self.add_review({'message': str(e)})
+      return False
+
+    if not self._must_run_all_tests() and affected_tests:
+      # Filter the test list for tests that are affected by the changed files
+      # and save this in step data.
+      test_list = [
+          test_cmd for test_cmd in test_list
+          if test_cmd.split()[0] in affected_tests
+      ]
+    self.add_step_data(step_data_key, test_list)
+
+    return True
+
+
+class RunAffectedUnitTestsStep(cast_shell_step.NonOtaBuildBaseStep):
+  """Build step class for running x86 unit tests, if any."""
+
+  def __init__(self,
+               branding,
+               build_type,
+               product,
+               build_flavor,
+               report_url,
+               recipe_steps,
+               sanitizer=None,
+               build_args_product=None,
+               build_args_official=None,
+               junit=False,
+               enable_network_service=False,
+               **kwargs):
+    """Instantiate the class.
+
+    Args:
+      branding: Build branding (chromium or chrome).
+      build_type: Type of system to build for ('x86', 'clang', or 'arm').
+      product: product to build for (ie. 'chromecast', 'audio').
+      build_flavor: Flavor of build ('Debug', 'Eng', 'Release').
+      report_url: The URL the code coverage report can be accessed at.
+      recipe_steps: Reference to the list of steps for this recipe.
+      sanitizer: Clang saniziter to run with ('asan', 'msan', 'tsan', 'ubsan').
+      build_args_product: The name of one of the
+        chromecast/internal/build/args/product files to use for the GN args.
+      junit: If true, run junit tests, otherwise run gtests (Default False).
+      enable_network_service: If true, include --enable-features=NetworkService
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    if junit:
+      step_name = 'run affected junit tests'
+    else:
+      step_name = 'run affected gtest unit tests'
+    cast_shell_step.NonOtaBuildBaseStep.__init__(
+        self,
+        step_name,
+        branding,
+        build_type,
+        product,
+        build_flavor,
+        sanitizer=sanitizer,
+        build_args_product=build_args_product,
+        build_args_official=build_args_official,
+        **kwargs)
+
+    assert isinstance(report_url, six.string_types)
+    assert isinstance(recipe_steps, list)
+    assert build_args_product
+
+    self._report_url = report_url
+    self._recipe_steps = recipe_steps
+    self._junit = junit
+    self._enable_network_service = enable_network_service
+    self._step_kwargs = kwargs
+
+    # Index to insert new steps at. This is to make sure that different steps that
+    # are included after the RunAffectedUnitTestsStep actually run after it,
+    # rather than the new steps being appended after them.
+    self._insert_index = None
+
+  def get_report_dir(self):
+    """Returns a unit test result directory."""
+    report_dir = os.path.join(self.get_gcs_dir(), UNIT_TEST_REPORT_PATH)
+    if not os.path.isdir(report_dir):
+      os.mkdir(report_dir)
+    return report_dir
+
+  def get_gtest_xml_log_uri(self, test_name):
+    """Returns a unit test xml log uri for the given gtest."""
+    return '{}/test_detail_{}.xml'.format(self.get_report_dir(), test_name)
+
+  def get_gtest_step(self, test_name, test_command):
+    """Returns a step that runs the unit test executable."""
+    if self._sanitizer:
+      return sanitizer_unittest_step.SanitizerUnitTestShellStep(
+          name=test_name,
+          command=test_command,
+          directory=self._outdir,
+          sanitizer=self._sanitizer,
+          **self._step_kwargs)
+
+    return shell_step.ShellStep(
+        name=test_name,
+        command=test_command,
+        directory=self._outdir,
+        **self._step_kwargs)
+
+  def get_junit_step(self, test_name):
+    """Returns a step that runs a junit test."""
+    return self.get_android_runner_step(test_name, 'junit')
+
+  def get_android_runner_step(self, test_name, test_type):
+    """Returns a step that runs an android test."""
+    test_runner = self._get_test_runner()
+    pythonpath = self._get_python_path()
+    # Single device, should be single test.
+    wrapper_command = [
+        'python3',
+        test_runner,
+        '--debug',  # Dumps test output to stdout.
+        '--type',
+        test_type,
+        '--test_result_dir',
+        self.get_gcs_dir(),
+        '--unit_test_binary_dir',
+        self._outdir,
+        '--unittest_name',
+        test_name
+    ]
+    return shell_step.ShellStep(
+        name=test_name,
+        command=wrapper_command,
+        directory='',
+        env={'PYTHONPATH': pythonpath},
+        **self._step_kwargs)
+
+  def get_json_step(self, test_name):
+    """Returns a step that generates test_result.json from existing xml."""
+    report_dir = self.get_report_dir()
+    test_project_dir = self.get_project_path(ANDROID_TEST_RUNNER_PROJECT)
+    xml_to_json = os.path.join(test_project_dir, TRANSFORM_XML_TO_JSON_PATH)
+    wrapper_command = [
+        'python3', xml_to_json, '--xml_dir_path', report_dir, '--branch_name',
+        self.manifest_branch, '--build_number', self.build_number,
+        '--test_name', test_name
+    ]
+    # TODO(b/73231972) remove unused flags once migrated to new builds
+    if self._build_flavor and self._product:
+      wrapper_command += [
+          '--build_flavor',
+          self._build_flavor,
+          '--product_name',
+          self._product,
+      ]
+    else:
+      wrapper_command += [
+          '--build_args_product',
+          self._build_args_product,
+      ]
+    return shell_step.ShellStep(
+        name='summary json for {}'.format(test_name),
+        command=wrapper_command,
+        directory='',
+        env={'PYTHONPATH': test_project_dir},
+        **self._step_kwargs)
+
+  def _add_step_to_recipe_steps(self, step):
+    if self._insert_index is None:
+      self._insert_index = self._recipe_steps.index(self) + 1
+
+    self._recipe_steps.insert(self._insert_index, step)
+    self._insert_index += 1
+
+  def add_gtest_tests(self):
+    test_list = self.get_step_data(STEP_DATA_TEST_LIST)
+
+    for test_exec_text in test_list:
+      test_command = test_exec_text.split(' ')
+      test_name = test_command[0]
+      test_command[0] = './' + test_name
+      if (self._enable_network_service and
+          branch_utils.is_branch_equal_to_or_later_than(self.manifest_branch,
+                                                        '1.41')):
+        test_command.append(
+            '--enable-features=NetworkService,NetworkServiceInProcess')
+        # TODO(b/130182642): Re-enable these tests once Chromium supports
+        # network service in unit tests.
+        disabled_tests = (':CastDevToolsManagerDelegateTest.*'
+                          ':CastMediaBlockerTest.*'
+                          ':CastSessionIdMapTest.*')
+        for i, part in enumerate(test_command):
+          if part.startswith('--gtest_filter=-'):
+            test_command[i] = part + disabled_tests
+            break
+          if i == len(test_command) - 1:
+            test_command.append('--gtest_filter=-' + disabled_tests)
+
+      test_command.append('--gtest_output=xml:{}'.format(
+          self.get_gtest_xml_log_uri(test_name)))
+      self._add_step_to_recipe_steps(
+          self.get_gtest_step(test_name, test_command))
+
+    self._add_step_to_recipe_steps(self.get_json_step('unit_test'))
+
+  def add_junit_tests(self):
+    test_list = self.get_step_data(STEP_DATA_JUNIT_TEST_LIST)
+    for test_exec_text in test_list:
+      test_name = test_exec_text.split(' ')[0]
+      self._add_step_to_recipe_steps(self.get_junit_step(test_name))
+
+  def _get_test_runner(self):
+    test_project_dir = self.get_project_path(ANDROID_TEST_RUNNER_PROJECT)
+    return os.path.join(test_project_dir, ANDROID_TEST_RUNNER_PATH)
+
+  def _get_python_path(self):
+    test_project_dir = self.get_project_path(ANDROID_TEST_RUNNER_PROJECT)
+    return ':'.join(
+        [test_project_dir,
+         self.get_project_path('continuous-tests')])
+
+  def run(self):
+    """Adds a separate step for each Chromium test case to be executed.
+
+    Returns:
+      True, it shouldn't be possible for this step to fail.
+    """
+
+    original_step_count = len(self._recipe_steps)
+    if self._junit:
+      self.add_junit_tests()
+    else:
+      self.add_gtest_tests()
+    new_steps_count = len(self._recipe_steps) - original_step_count
+    if new_steps_count:
+      logging.info('Running %s tests affecting this change.', new_steps_count)
+    else:
+      logging.info('No affected tests were detected for this change. None '
+                   'will be run.')
+
+    return True
diff --git a/cq/scripts/slave/step/x86_camera_step.py b/cq/scripts/slave/step/x86_camera_step.py
new file mode 100644
index 0000000..c065d1b
--- /dev/null
+++ b/cq/scripts/slave/step/x86_camera_step.py
@@ -0,0 +1,463 @@
+from __future__ import absolute_import
+import os
+import re
+import collections
+
+from slave import base_step
+from slave.step import code_coverage_step
+
+BUILD_VARIANT = 'eng'
+
+TARGET_PRODUCT = {
+  'gq': 'gq_x86_64',
+  'newman': 'newman_x86_64',
+  'nq': 'nq_x86_64',
+  'nq_exp': 'nq_exp_x86_64',
+  'spencer': 'spencer_x86_64',
+  'venus': 'venus_x86_64',
+}
+
+BUILD_ITEM = {
+  'gq': ['curl', 'nldaemon', 'nldaemon-cli', 'chromium-iot'],
+  'newman': 'reconnect-tests',
+  'nq': ['curl', 'nldaemon', 'nldaemon-cli', 'chromium-iot'],
+  'nq_exp': ['curl', 'nldaemon', 'nldaemon-cli', 'chromium-iot',
+             'libcast_camera_hal', 'libamvenc_api_c328', 'libdma_api_c328',
+             'libcutils'],
+  'spencer': ['curl', 'nldaemon', 'nldaemon-cli', 'chromium-iot'],
+  'venus': ['curl', 'nldaemon', 'nldaemon-cli', 'chromium-iot'],
+  'iot-tests': 'chromium-iot-tests'
+}
+
+_MAKE_ERROR_PATTERN = (
+  r'^(?P<filename>[^:]+):(?P<linenum>\d+):(?P<column>\d+): '
+  'error: (?P<err_msg>.*)$',
+  r'^(?P<filename>[^\-]+)-(?P<linenum>\d+): error: (?P<err_msg>.*)$',
+)
+
+IOT_TESTS_LIST = 'tests/iot/run_test_list.txt'
+
+OUT_DIR = 'out/target/product/newman_x86_64/system/bin'
+NCCM_OUT_DIR = 'chromium/src/out/Release/'
+NCCM_BUILD_ITEM = os.path.join("chromecast/internal/iot_services",
+                               "nestcam_context_manager:nccm_unittests")
+
+PHOTON_OUT_DIR = 'chromium/src/out/Release/'
+PHOTON_BUILD_ITEMS = [ os.path.join("chromecast/internal/iot_services",
+                                    "photon_hal:photon_hal_group"),
+                       os.path.join("chromecast/internal/iot_services",
+                                    "photon_hal:photon_hal_tests") ]
+
+SIM_SYSTEM = [
+  "out/target/product/{}/system/bin/",
+  "out/target/product/{}/system/etc/",
+  "out/target/product/{}/system/lib/",
+  "out/target/product/{}/symbols/system/chrome/bin/",
+  "out/target/product/{}/symbols/system/chrome/lib/",
+]
+SIM_LIB = [
+  "chromium/src/build/linux/debian_sid_amd64-sysroot/usr/lib/x86_64-linux-gnu/",
+  os.path.join("chromium/src/build/linux/debian_sid_amd64-sysroot/lib",
+               "x86_64-linux-gnu/libcom_err.so.2.1"),
+  os.path.join("chromium/src/build/linux/debian_sid_amd64-sysroot/lib",
+               "x86_64-linux-gnu/libkeyutils.so.1.9"),
+  os.path.join("chromium/src/third_party/android_ndk/toolchains/llvm/prebuilt",
+               "linux-x86_64/lib64/libc++.so.1"),
+]
+SIM_CONFIG = "virtual-home/cirque-park/build_eureka/{}/"
+MOJO_EVENTS = "virtual-home/cirque-park/build_eureka/mojo_events/"
+SAMPLE_VIDEO = "vendor/nest/reconnect/data/test/proxy_video.h264"
+SAMPLE_AUDIO = "vendor/nest/reconnect/data/test/proxy_audio_mono.opus"
+SAMPLE_JPEG = "vendor/nest/reconnect/data/test/{}_sample.jpeg"
+
+CODE_COVERAGE_GN_ARGS = 'use_clang_coverage=true is_component_build=false'
+
+def make_error_to_comments(stderr, directory):
+  """
+  parse stderr with regex error pattern and return comments dict
+  Args:
+    stderr (str): logs from stderr
+  Return:
+    (dict) comments: {
+      "filename":
+        { "linenum": <int>,
+          "message": <str> error message,
+        }
+  }
+  """
+  comments = collections.defaultdict(list)
+
+  for line in stderr.splitlines():
+    match = [m for m in [
+        re.match(err_pat, line) for err_pat in _MAKE_ERROR_PATTERN
+        ] if m is not None]
+    if match:
+      match = match[0]
+      filename = match.group('filename')
+      if directory and filename.startswith(directory):
+        filename = filename[len(directory) + len(os.path.sep):]
+      linenum = int(match.group('linenum'))
+      comments[filename].append({'line': linenum, 'message': line})
+  return comments
+
+
+class MakeCleanSim(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    self.product = kwargs['product']
+    base_step.BaseStep.__init__(self,
+                                name='make clean x86_64 {}'.format(
+                                  self.product.upper()),
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+
+    command = [
+        'make',
+        'TARGET_PRODUCT={}'.format(TARGET_PRODUCT[self.product]),
+        'TARGET_BUILD_VARIANT={}'.format(BUILD_VARIANT),
+        'clean'
+    ]
+    returncode, _, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      comment = make_error_to_comments(stderr, '')
+      self.add_review({
+          'comments': comment
+      })
+      return False
+
+    return True
+
+
+class BuildSimulatorStep(base_step.BaseStep):
+
+  def __init__(self, code_coverage=False, **kwargs):
+    self.product = kwargs['product']
+    self._code_coverage = code_coverage
+    base_step.BaseStep.__init__(self,
+                                name='build x86_64 {}'.format(
+                                  self.product.upper()),
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+    command = [
+        'make',
+        '-j{}'.format(self.get_num_jobs()),
+        'TARGET_PRODUCT={}'.format(TARGET_PRODUCT[self.product]),
+        'TARGET_BUILD_VARIANT={}'.format(BUILD_VARIANT)
+    ]
+
+    if self._code_coverage:
+      command.append(
+        'COMMAND_LINE_GN_ARGS=\'{}\''.format(CODE_COVERAGE_GN_ARGS))
+
+    command += self.build_accelerator.make_flags
+    command += BUILD_ITEM[self.product]
+    returncode, _, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      comment = make_error_to_comments(stderr, '')
+      self.add_review({
+          'comments': comment
+      })
+      return False
+    return True
+
+
+class CreateSimTarStep(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    self.product = kwargs['product']
+    base_step.BaseStep.__init__(self,
+                                name='tar-ing sim artifacts',
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+    tar_filename = '{}.tar.gz'.format(TARGET_PRODUCT[self.product])
+    command = [
+      'tar',
+      'czf',
+      os.path.join(self.get_gcs_dir(), tar_filename),
+      SIM_CONFIG.format("{}_x86".format(self.product)),
+      MOJO_EVENTS,
+      SAMPLE_VIDEO,
+      SAMPLE_AUDIO,
+      SAMPLE_JPEG.format(self.product)
+    ]
+    command += [path.format(TARGET_PRODUCT[self.product]) for path in SIM_SYSTEM]
+    command += SIM_LIB
+    returncode, _, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      comment = make_error_to_comments(stderr, '')
+      self.add_review({
+        'comments': comment
+      })
+    else:
+      self.set_build_property('ota_files', [tar_filename])
+
+    return True
+
+
+class BuildChromiumIotTestsStep(base_step.BaseStep):
+
+  def __init__(self, code_coverage=False, **kwargs):
+    self.product = kwargs['product']
+    self.out_dir = kwargs['out_dir']
+    self._code_coverage = code_coverage
+    base_step.BaseStep.__init__(self,
+                                name='build chromium-iot-tests',
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+
+    command = [
+        'make',
+        '-j{}'.format(self.get_num_jobs()),
+        'TARGET_PRODUCT={}'.format(TARGET_PRODUCT[self.product]),
+        'TARGET_BUILD_VARIANT={}'.format(BUILD_VARIANT),
+        '{}'.format(BUILD_ITEM['iot-tests'])
+    ]
+
+    if self._code_coverage:
+      command.append(
+        'COMMAND_LINE_GN_ARGS=\'{}\''.format(CODE_COVERAGE_GN_ARGS))
+
+    command += self.build_accelerator.make_flags
+
+    returncode, _, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      comment = make_error_to_comments(stderr, '')
+      self.add_review({
+          'comments': comment
+      })
+      return False
+
+    test_list = self.exec_read_file(
+      os.path.join(self.out_dir, IOT_TESTS_LIST)).splitlines()
+    self.add_step_data(code_coverage_step.STEP_DATA_TEST_LIST, test_list)
+    return True
+
+
+class RunChromiumIotTestsStep(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    self.cwd = kwargs['cwd']
+    self.product = kwargs['product']
+    base_step.BaseStep.__init__(self,
+                                name='run chromium-iot-tests',
+                                halt_on_failure=True,
+                                **kwargs)
+  def get_commands(self):
+    commands = []
+    with open(os.path.join(self.cwd, IOT_TESTS_LIST), 'r') as f:
+      for line in f:
+        commands.append(["./{}".format(line.rstrip())])
+    return commands
+
+  def run(self):
+    commands = self.get_commands()
+    env = {
+        'LD_LIBRARY_PATH': ":".join(
+            ['../../../../prebuilt/toolchain/x86_64/usr/lib64',
+             os.path.join('../../../../out/target/product',
+                          '{}/system/lib'.format(TARGET_PRODUCT[self.product]))
+            ])
+    }
+    for command in commands:
+      returncode, _, _ = self.exec_subprocess(command, cwd=self.cwd, env=env)
+      # return early if any unittest fails
+      if returncode != 0:
+        return False
+    return True
+
+
+class BuildReconnectProxyTestStep(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    self.product = kwargs['product']
+    base_step.BaseStep.__init__(self,
+                                name='build reconnect-tests',
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+
+    command = [
+        'make',
+        '-j{}'.format(self.get_num_jobs()),
+        'TARGET_PRODUCT={}'.format(TARGET_PRODUCT[self.product]),
+        'TARGET_BUILD_VARIANT={}'.format(BUILD_VARIANT),
+        '{}'.format(BUILD_ITEM[self.product])
+    ]
+    returncode, _, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      comment = make_error_to_comments(stderr, '')
+      self.add_review({
+          'comments': comment
+      })
+      return False
+    return True
+
+
+class RunReconnectTestsStep(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    base_step.BaseStep.__init__(self,
+                                name='run reconnect-tests',
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+
+    command = [
+        './{}'.format(os.path.join(OUT_DIR, 'reconnect-tests'))
+    ]
+    returncode, _, _ = self.exec_subprocess(command)
+    return returncode == 0
+
+
+class BuildNccmUnitTestStep(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    self.out_dir = kwargs.get('out_dir', NCCM_OUT_DIR)
+    base_step.BaseStep.__init__(self,
+                                name='build nccm_unittests',
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+
+    command = [
+        'autoninja',
+        '-C',
+        self.out_dir,
+        NCCM_BUILD_ITEM
+    ]
+    returncode, _, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      comment = make_error_to_comments(stderr, '')
+      self.add_review({
+          'comments': comment
+      })
+      return False
+
+    self.add_step_data(
+      code_coverage_step.STEP_DATA_TEST_LIST, ['nccm_unittests'])
+    return True
+
+
+class RunNccmUnitTestStep(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    self.out_dir = kwargs['out_dir']
+    base_step.BaseStep.__init__(self,
+                                name='run nccm_unittests',
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+
+    command = [
+        ('./{}').format(os.path.join(self.out_dir, 'nccm_unittests'))]
+    returncode, _, _ = self.exec_subprocess(command)
+    return returncode == 0
+
+
+class BuildAndRunNldaemonUnitTestStep(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    self.product = kwargs['product']
+    super().__init__(
+        name='run nldaemon_unittests',
+        halt_on_failure=True,
+        **kwargs,
+    )
+
+  def run(self):
+    if self.product != 'gq':
+      # gq_x86_64 is experimental as of 2020-Dec-17
+      # Not blocking others if the tests fails.
+      return True
+
+    # We need to do a clean build so the coverage data can be generated correctly.
+    command = [
+        'make',
+        'TARGET_PRODUCT={}'.format(TARGET_PRODUCT[self.product]),
+        'TARGET_BUILD_VARIANT={}'.format(BUILD_VARIANT),
+        'clean'
+    ]
+    returncode, _, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      comment = make_error_to_comments(stderr, '')
+      self.add_review({
+          'comments': comment
+      })
+      return False
+
+    env = {
+        'LD_LIBRARY_PATH': ":".join(
+            [os.path.join('chromium/src/third_party/android_ndk',
+                          'toolchains/llvm/prebuilt/linux-x86_64/lib64'),
+             os.path.join('out/target/product',
+                          '{}/system/lib'.format(TARGET_PRODUCT[self.product]))
+            ])
+    }
+    command = [
+        'make',
+        '-j{}'.format(self.get_num_jobs()),
+        'ENABLE_CODE_COVERAGE=1',
+        'TARGET_PRODUCT={}'.format(TARGET_PRODUCT[self.product]),
+        'TARGET_BUILD_VARIANT={}'.format(BUILD_VARIANT),
+        'nldaemon_unittests_run'
+    ]
+    returncode, _, stderr = self.exec_subprocess(command, env=env)
+    if returncode != 0:
+      comment = make_error_to_comments(stderr, '')
+      self.add_review({
+          'comments': comment
+      })
+      return False
+    return True
+
+class BuildPhotonStep(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    self.out_dir = kwargs.get('out_dir', PHOTON_OUT_DIR)
+    base_step.BaseStep.__init__(self,
+                                name='build photon',
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+    command = [
+        'autoninja',
+        '-C',
+        self.out_dir
+    ]
+    command += PHOTON_BUILD_ITEMS
+    returncode, _, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      comment = make_error_to_comments(stderr, '')
+      self.add_review({
+          'comments': comment
+      })
+      return False
+    return True
+
+class RunPhotonUnitTestStep(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    self.out_dir = kwargs['out_dir']
+    base_step.BaseStep.__init__(self,
+                                name='run photon_hal_tests',
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+    command = [
+        ('./{}').format(os.path.join(self.out_dir, 'photon_hal_tests'))]
+    returncode, _, _ = self.exec_subprocess(command)
+    return returncode == 0
diff --git a/cq/scripts/slave/step/x86_cast_camera_hal_step.py b/cq/scripts/slave/step/x86_cast_camera_hal_step.py
new file mode 100644
index 0000000..9309854
--- /dev/null
+++ b/cq/scripts/slave/step/x86_cast_camera_hal_step.py
@@ -0,0 +1,166 @@
+from __future__ import absolute_import
+import collections
+import logging
+import os
+import re
+import shutil
+
+from slave import base_step
+
+BUILD_VARIANT = 'eng'
+
+TARGET_PRODUCT = {
+  'nq': 'nq_exp_x86_64',
+}
+
+TEST_TARGET = 'cast_camera_hal_host_test'
+
+MAKE_ERROR_PATTERN = (
+  r'^(?P<filename>[^:]+):(?P<linenum>\d+):(?P<column>\d+): '
+  'error: (?P<err_msg>.*)$',
+  r'^(?P<filename>[^\-]+)-(?P<linenum>\d+): error: (?P<err_msg>.*)$',
+)
+
+OUT_DIR = 'out/host/linux-x86/bin'
+
+def make_error_to_comments(stderr, directory):
+  """
+  parse stderr with regex error pattern and return comments dict
+  Args:
+    stderr (str): logs from stderr
+  Return:
+    (dict) comments: {
+      "filename":
+        { "linenum": <int>,
+          "message": <str> error message,
+        }
+  }
+  """
+  comments = collections.defaultdict(list)
+
+  for line in stderr.splitlines():
+    match = [m for m in [
+        re.match(err_pat, line) for err_pat in MAKE_ERROR_PATTERN
+        ] if m is not None]
+    if match:
+      match = match[0]
+      filename = match.group('filename')
+      if directory and filename.startswith(directory):
+        filename = filename[len(directory) + len(os.path.sep):]
+      linenum = int(match.group('linenum'))
+      comments[filename].append({'line': linenum, 'message': line})
+  return comments
+
+class BuildCastCameraHalTestStep(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    base_step.BaseStep.__init__(self,
+                                name='build {}'.format(TEST_TARGET),
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+
+    # TODO(b/240648405): remove TARGET_PRODUCT flag
+    command = [
+        'make',
+        '-j{}'.format(self.get_num_jobs()),
+        'TARGET_PRODUCT={}'.format(TARGET_PRODUCT['nq']),
+        'TARGET_BUILD_VARIANT={}'.format(BUILD_VARIANT),
+        '{}'.format(TEST_TARGET)
+    ]
+    returncode, _, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      comment = make_error_to_comments(stderr, '')
+      self.add_review({
+          'comments': comment
+      })
+      return False
+
+    return True
+
+class RunCastCameraHalTestStep(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    base_step.BaseStep.__init__(self,
+                                name='run {}'.format(TEST_TARGET),
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+
+    command = [
+        './{}'.format(os.path.join(OUT_DIR, TEST_TARGET))
+    ]
+    returncode, _, _ = self.exec_subprocess(command)
+    return returncode == 0
+
+class InstallCoverageTools(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    base_step.BaseStep.__init__(self,
+                                name="install coverage tools",
+                                halt_on_failure=True,
+                                **kwargs)
+  def run(self):
+    # python3 chromium/src/tools/clang/scripts/update.py --package \
+    # coverage_tools
+    vpython3_path = os.path.join(
+        self.get_project_path('chromium/tools/depot_tools'), 'vpython3')
+    update_py_path = os.path.join(
+        self.get_project_path('chromium/src'), 'tools/clang/scripts/update.py')
+    command = [vpython3_path, update_py_path, '--package', 'coverage_tools']
+    returncode, _, _ = self.exec_subprocess(command)
+    return returncode == 0
+
+class CollectCameraHalTestCoverageStep(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    base_step.BaseStep.__init__(self,
+                                name="generate coverage data",
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+    # generate coverage report
+    eureka_dir = os.getcwd()
+    output_dir = os.path.join(eureka_dir, 'code_coverage_html','linux')
+    make_output_dir_command = ['mkdir', '-p', output_dir]
+    returncode, _, _ = self.exec_subprocess(make_output_dir_command)
+    if returncode != 0:
+      self.add_review({
+          'comments': 'Failed to create output directory: {}'.format(output_dir)
+      })
+      return False
+
+    cast_camera_hal_dir = os.path.join(eureka_dir,
+                                       'hardware',
+                                       'cast-camera-hal')
+    cast_camera_hal_test_dir = os.path.join(cast_camera_hal_dir,
+                                            'test')
+    coverage_collecting_script = os.path.join(cast_camera_hal_test_dir,
+                                              'analyze-test-coverage.sh')
+    manifest_file = os.path.join(cast_camera_hal_test_dir,
+                                 'camera-hal-host-coverage-manifest.txt')
+
+    collect_coverage_data_command = [
+        '{}'.format(coverage_collecting_script),
+        '-u{}'.format(cast_camera_hal_dir),
+        '-b{}'.format(eureka_dir),
+        '-g{}'.format(manifest_file),
+        '-o{}'.format(output_dir)
+    ]
+
+    returncode, _, _ = self.exec_subprocess(
+        collect_coverage_data_command,
+        env={"ANDROID_BUILD_TOP": eureka_dir})
+
+    try:
+      copied_report_dir = os.path.join(self.get_gcs_dir(), 'code_coverage_html')
+      shutil.copytree(output_dir, copied_report_dir)
+      out_files = os.listdir(copied_report_dir)
+      logging.info('Generated files: %s', str(out_files))
+    except IOError:
+      logging.warning('Failed to copy coverage output to GCS')
+
+    return returncode == 0
diff --git a/cq/scripts/slave/step/x86_integration_test_step.py b/cq/scripts/slave/step/x86_integration_test_step.py
new file mode 100644
index 0000000..8e804e7
--- /dev/null
+++ b/cq/scripts/slave/step/x86_integration_test_step.py
@@ -0,0 +1,221 @@
+"""Step class to run tests against a local x86 cast shell."""
+
+from __future__ import absolute_import
+import datetime
+import glob
+import json
+import logging
+import os
+import time
+import traceback
+
+from helpers import cast_shell_utils
+from helpers import py_env_utils
+from slave import base_step
+
+_FRAGGLE_ROCK_CONFIG_FILES_ENV_VAR = 'FRAGGLE_ROCK_CONFIG_FILES'
+_LOG_DIR = 'integration_test_logs'
+
+
+# pylint: disable=method-hidden
+
+
+class RunIntegrationTestStep(base_step.BaseStep):
+  """Step for running integration test against a local x86 cast shell."""
+
+  def __init__(self, test_case_class, test_case_module, test_sequence_class,
+               test_sequence_module, test_config_file_path,
+               device_class='LocalX86CastShellDevice',
+               device_module='eurtest.device.local_x86_cast_shell_device',
+               assistant_enabled=False,
+               headless=False,
+               debug_output=True,
+               **kwargs):
+    """Creates a RunIntegrationTestStep instance.
+
+    Args:
+      test_case_class: A list of TestCase implementations to run.  Each TestCase
+        class is expected to be  defined in the test_case_module.
+      test_case_module: Module containing TestCase implementation to run.
+      test_sequence_class: TestSequence implementation to run.
+      test_sequence_module: Module containing TestSequence implementation to
+        run.
+      test_config_file_path: Dot-notation path to the test's config file
+        relative to config_file_root_dir.
+      device_class: A string with the name of Python class that represents the
+        device.
+      device_module: Dot-notation path to the module containing |device_class|.
+      assistant_enabled: Whether the x86 cast_shell against which tests will
+        be run is assistant-enabled (in which case, some special flags are
+        specified with the cast_shell is started).
+      headless: Whether the x86 cast_shell uses headless ozone platform.
+      debug_output: Log debug output from the test.
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='Run integration tests', **kwargs)
+    self._test_case_class = test_case_class
+    self._test_case_module = test_case_module
+    self._test_sequence_class = test_sequence_class
+    self._test_sequence_module = test_sequence_module
+    self._test_config_file_path = test_config_file_path
+    self._device_class = device_class
+    self._device_module = device_module
+    self._device_ip = '127.0.0.1'
+    self._config_file_root_dir = os.path.join(
+        self.get_project_path('continuous-tests'), 'lab_system', 'master',
+        'config')
+    self._assistant_enabled = assistant_enabled
+    self._headless = headless
+    self._debug_output = debug_output
+
+  def _remove_existing_results_json(self):
+    """Remove any results JSON files from previous runs."""
+    previous_results = glob.glob(os.path.join(os.getcwd(), '*results.json'))
+    for result_found in previous_results:
+      os.remove(result_found)
+
+  def _execute_tests(self, venv_root):
+    """Execute the integration tests using eurtest.framework.test_runner.
+
+    Args:
+      venv_root: Path to virtual environment root directory.
+
+    Returns:
+      A Tuple of (a dict representing a review object,
+                  a boolean indicating if tests passed)
+    """
+    self._remove_existing_results_json()
+
+    short_wait_secs = 5
+    env = {
+        _FRAGGLE_ROCK_CONFIG_FILES_ENV_VAR: self._config_file_root_dir,
+        'PYTHONPATH': self._get_python_path()
+    }
+    runner = os.path.abspath(
+        os.path.join(self.get_project_path('test'),
+                     'eurtest', 'framework', 'test_runner.py'))
+    run_info = {}
+    run_info['start_timestamp'] = datetime.datetime.now().isoformat()
+    run_info['branch_name'] = self.manifest_branch
+    run_info['platform'] = 'clang_x86'
+    cmd = [
+        runner,
+        '--test_case_class', ','.join(self._test_case_class),
+        '--test_case_module', self._test_case_module,
+        '--test_sequence_class', self._test_sequence_class,
+        '--test_sequence_module', self._test_sequence_module,
+        '--target_device_ethernet_ip', self._device_ip,
+        '--target_device_wifi_ip', self._device_ip,
+        '--device_class', self._device_class,
+        '--device_module', self._device_module,
+        '--run_information_json', json.dumps(run_info),
+        '--test_config_file_path', self._test_config_file_path,
+        '--nouse_tunnel'
+    ]
+    if self._debug_output:
+      cmd.append('--debug')
+    returncode, stdout, stderr = py_env_utils.exec_within_virtual_env(
+        self, venv_root, cmd, env=env)
+    if returncode != 0:
+      logging.error('Failed to run tests: %s\n%s', stdout, stderr)
+
+    review = ({'message': 'no result found'}, False)
+    # A JSON file will be saved to results_path directory within a few seconds
+    time.sleep(short_wait_secs)
+    results = glob.glob(os.path.join(os.getcwd(), '*results.json'))
+    if results:
+      # Assumption: there should be only one results JSON file
+      review = self.generate_review(results[0])
+    return review
+
+  def generate_review(self, results):
+    """Generate a review object from integration test results.
+
+    Args:
+      results: File path to result JSON file.
+
+    Returns:
+      A Tuple of (a dict representing a review object,
+                  a boolean indicating if tests passed)
+    """
+    passing = True
+    result_messages = []
+    results_data = []
+    summary = 'Suites: {}, Run: {}, Errors: {}, Failures: {}'
+    with open(results) as results_json:
+      json_output = json.load(results_json)
+      if isinstance(json_output, list):
+        results_data.extend(json_output)
+      else:
+        results_data.append(json_output)
+    for result in results_data:
+      suite_count = 0
+      test_count = 0
+      failure_count = 0
+      error_count = 0
+      for suite in result['test_suites']:
+        suite_count += 1
+        test_count += suite['test_count']
+        failure_count += suite['failure_count']
+        error_count += suite['error_count']
+      if failure_count + error_count > 0:
+        message = ('Test: {}, {}, Time Taken(sec): {:.2f}').format(
+            result['test_name'],
+            summary.format(suite_count, test_count, error_count, failure_count),
+            result['elapsed_secs'])
+        result_messages.append(message)
+        passing = False
+    if passing:
+      status = 'Looks Good. All tests passed.'
+    else:
+      status = 'Cast compliance tests failed. See the build logs for details.'
+    review = {
+        'message': ('{} \n{}'.format(status, '\n'.join(result_messages)))
+    }
+    return review, passing
+
+  def _get_python_path(self):
+    """Returns the PYTHONPATH to execute tests with."""
+    projects = ['test', 'continuous-tests']
+    python_paths = [
+        os.path.abspath(self.get_project_path(project)) for project in projects]
+    return os.pathsep.join(python_paths)
+
+  def run(self):
+    """Starts a local x86 cast shell and run integration tests against it.
+
+    Returns:
+      True iff all tests pass.
+    """
+    log_fd = None
+    log_file_path = None
+    try:
+      # Launch cast_shell
+      _, log_fd, log_file_path = cast_shell_utils.start_cast_shell(
+          self,
+          self.get_project_path('test'),
+          self.get_step_data(cast_shell_utils.CAST_SHELL_EXE_PATH),
+          self.get_tmp_dir(),
+          enable_cma_media_pipeline=True,
+          use_headless_ozone_platform=(
+              self._headless or self._assistant_enabled),
+          launch_utility_process=self._assistant_enabled)
+      venv_root = self.get_step_data('virtual_env_root')
+      review_message, is_passing = self._execute_tests(venv_root)
+    except:  # pylint: disable=bare-except
+      review_message = {
+          'message': ('Cast compliance test setup failed.\n{}'.format(
+              traceback.format_exc()))
+      }
+      is_passing = False
+
+    if log_fd and log_file_path:
+      cast_shell_utils.upload_cast_shell_logs(log_fd,
+                                              log_file_path,
+                                              self.get_gcs_dir(), _LOG_DIR,
+                                              'cast_shell_console.txt')
+    if not is_passing:
+      self.add_review(review_message)
+    else:
+      logging.info(review_message.get('message'))
+    return is_passing
diff --git a/cq/scripts/slave/step/x86_manhattan_test_integration_step.py b/cq/scripts/slave/step/x86_manhattan_test_integration_step.py
new file mode 100644
index 0000000..f7d81f5
--- /dev/null
+++ b/cq/scripts/slave/step/x86_manhattan_test_integration_step.py
@@ -0,0 +1,104 @@
+"""Step class to run crash tests against a local x86 Manhattan shell."""
+
+from __future__ import absolute_import
+import logging
+import time
+
+from helpers import cast_shell_utils
+from slave import base_step
+
+_LOG_DIR = 'crash_test_logs'
+
+
+class RunManhattanShellCrashTestStep(base_step.BaseStep):
+  """Step for running crash test against a local x86 cast shell."""
+
+  def __init__(self, **kwargs):
+    """Creates a RunManhattanShellCrashTestStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self,
+                                name='Run crash loop integration test',
+                                **kwargs)
+    self.wait_time = 30
+    self.timeout = 300
+
+  def _starting_cast_shell(self):
+    """Starts a local x86 cast shell and get its process Id.
+
+    Returns:
+      A tuple of cast shell status, process id, log file descriptor and log file
+      path.
+    """
+    status = True
+
+    try:
+      subprocess, log_file_descriptor, log_file_path = (
+          cast_shell_utils.start_cast_shell(
+              self,
+              self.get_project_path('test'),
+              self.get_step_data(cast_shell_utils.CAST_SHELL_EXE_PATH),
+              self.get_tmp_dir(),
+              enable_cma_media_pipeline=False,
+              use_headless_ozone_platform=True,
+              launch_utility_process=True))
+    except (cast_shell_utils.CastShellError,
+            cast_shell_utils.CastShellTimeoutError):
+      logging.exception('Failed to start cast_shell')
+      status = False
+
+    return status, subprocess.pid, log_file_descriptor, log_file_path
+
+  def _is_cast_shell_still_running(self, cast_shell_running_pid):
+    """Verify cast_shell is still running.
+
+    Args:
+      cast_shell_running_pid: Started cast_shell process id.
+    Returns:
+      boolean indicating if cast_shell is still running.
+    """
+    current_running_processes = self.get_running_subprocess_pids()
+    if cast_shell_running_pid not in current_running_processes:
+      return False
+    device_name = cast_shell_utils.get_device_name(self)
+    logging.info('Retrieved Device Name: %s', device_name)
+    if not device_name:
+      return False
+    return True
+
+  def _timeout_wait(self, start_time):
+    """Returns True if waiting for a timeout, returns False others."""
+    elapsed = time.time() - start_time
+    if elapsed < self.timeout:
+      logging.info(
+          '%s seconds have elapsed, %s second remaining, waiting %s seconds',
+          elapsed, self.timeout - elapsed, self.wait_time)
+      time.sleep(self.wait_time)
+      return True
+    return False
+
+  def run(self):
+    """Run test against x86 cast shell every 30 sec. to confirm its running.
+
+    Returns:
+      True if all tests pass.
+    """
+    start_time = time.time()
+
+    is_passing, cast_shell_pid, log_file_descriptor, log_file_path = (
+        self._starting_cast_shell())
+
+    while self._timeout_wait(start_time):
+      if not self._is_cast_shell_still_running(cast_shell_pid):
+        is_passing = False
+        logging.info('Manhattan cast_shell failed after it started')
+        break
+
+    if log_file_descriptor and log_file_path:
+      cast_shell_utils.upload_cast_shell_logs(log_file_descriptor,
+                                              log_file_path,
+                                              self.get_gcs_dir(), _LOG_DIR,
+                                              'cast_shell_console.txt')
+    return is_passing
diff --git a/cq/scripts/slave/step/x86_wifi_step.py b/cq/scripts/slave/step/x86_wifi_step.py
new file mode 100644
index 0000000..ead9c01
--- /dev/null
+++ b/cq/scripts/slave/step/x86_wifi_step.py
@@ -0,0 +1,235 @@
+from __future__ import absolute_import
+import os
+import pathlib
+import re
+import collections
+
+from slave import base_step
+from slave.step import code_coverage_step
+
+BUILD_VARIANT = 'eng'
+TARGET_PRODUCT = 'sirocco_x86_64'
+BUILD_ITEM = 'sirocco_virtual_device_deps'
+UNIT_TESTS = [
+  'ap-wireless_unittest',
+  'ap-infra_unittests',
+  'ap-daemons_unittest',
+]
+
+_MAKE_ERROR_PATTERN = (
+  r'^(?P<filename>[^:]+):(?P<linenum>\d+):(?P<column>\d+): '
+  'error: (?P<err_msg>.*)$',
+  r'^(?P<filename>[^\-]+)-(?P<linenum>\d+): error: (?P<err_msg>.*)$',
+)
+
+_GTEST_ERROR_TEST_NAME_PATTERN = re.compile(r'^\[. FAILED. \] (\S+\.\S+)$',
+                                            re.MULTILINE)
+
+def make_error_to_comments(stderr, directory):
+  """
+  parse stderr with regex error pattern and return comments dict
+  Args:
+    stderr (str): logs from stderr
+  Return:
+    (dict) comments: {
+      "filename":
+        { "linenum": <int>,
+          "message": <str> error message,
+        }
+  }
+  """
+  comments = collections.defaultdict(list)
+
+  for line in stderr.splitlines():
+    match = [m for m in [
+        re.match(err_pat, line) for err_pat in _MAKE_ERROR_PATTERN
+        ] if m is not None]
+    if match:
+      match = match[0]
+      filename = match.group('filename')
+      if directory and filename.startswith(directory):
+        filename = filename[len(directory) + len(os.path.sep):]
+      linenum = int(match.group('linenum'))
+      comments[filename].append({'line': linenum, 'message': line})
+  return comments
+
+
+class MakeCleanSim(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    base_step.BaseStep.__init__(self,
+                                name='make clean sirocco_x86_64',
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+
+    command = [
+        'make',
+        'TARGET_PRODUCT={}'.format(TARGET_PRODUCT),
+        'TARGET_BUILD_VARIANT={}'.format(BUILD_VARIANT),
+        'clean'
+    ]
+    returncode, _, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      comment = make_error_to_comments(stderr, '')
+      self.add_review({
+          'comments': comment
+      })
+      return False
+
+    return True
+
+
+class BuildSimulatorStep(base_step.BaseStep):
+
+  def __init__(self, code_coverage=False, **kwargs):
+    self._code_coverage = code_coverage
+    base_step.BaseStep.__init__(self,
+                                name='build x86_64 sirocco_x86_64',
+                                halt_on_failure=True,
+                                **kwargs)
+
+  def run(self):
+    command = [
+        'make',
+        '-j{}'.format(self.get_num_jobs()),
+        'TARGET_PRODUCT={}'.format(TARGET_PRODUCT),
+        'TARGET_BUILD_VARIANT={}'.format(BUILD_VARIANT)
+    ]
+
+    command += self.build_accelerator.make_flags
+    command += [BUILD_ITEM]
+    returncode, _, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      comment = make_error_to_comments(stderr, '')
+      self.add_review({
+          'comments': comment
+      })
+      return False
+
+    return True
+
+class BuildUnitTestsStep(base_step.BaseStep):
+
+  def __init__(self, code_coverage=False, **kwargs):
+    self._code_coverage = code_coverage
+    super().__init__(name='build unittests sirocco_x86_64',
+                     halt_on_failure=True,
+                     **kwargs)
+
+  def run(self):
+    command = [
+        'make',
+        '-j{}'.format(self.get_num_jobs()),
+        'TARGET_PRODUCT={}'.format(TARGET_PRODUCT),
+        'TARGET_BUILD_VARIANT={}'.format(BUILD_VARIANT),
+    ]
+
+    if self._code_coverage:
+      command.append('COMMAND_LINE_GN_ARGS=\''
+        'use_clang_coverage=true is_component_build=false\''
+      )
+
+    if self.build_accelerator.make_flags:
+      command.extend(self.build_accelerator.make_flags)
+    command.append('sirocco_virtual_unittests')
+    returncode, _, stderr = self.exec_subprocess(command)
+    if returncode:
+      comment = make_error_to_comments(stderr, '')
+      self.add_review({
+          'comments': comment
+      })
+      return False
+    test_list = UNIT_TESTS.copy()
+    self.add_step_data(code_coverage_step.STEP_DATA_TEST_LIST, test_list)
+    return True
+
+class RunUnitTestsStep(base_step.BaseStep):
+
+  def __init__(self, test_env=None, build_out_dir=None, **kwargs):
+    self._test_env = test_env
+    self._build_out_dir = build_out_dir
+    super().__init__(name='run unittests sirocco_x86_64',
+                     halt_on_failure=True,
+                     **kwargs)
+
+  def run(self):
+    # Use to run all tests, then if any failed we fail this step
+    errors = []
+    for test in UNIT_TESTS:
+      returncode, stdout, _ = self.exec_subprocess(
+        [f'{self._build_out_dir}/{test}'],
+        cwd='chromium/src',
+        env=self._test_env
+      )
+      if returncode:
+        gtest_failures = re.findall(_GTEST_ERROR_TEST_NAME_PATTERN, stdout)
+        if gtest_failures:
+          errors.extend(
+            [f'{test_case} from {test}' for test_case in gtest_failures]
+          )
+        else:
+          errors.append(f'Test execution error for {test}')
+    if errors:
+      error_msg = '\n'.join(errors)
+      self.add_review({
+        'message': f'{len(errors)} failure(s) from unit tests:\n{error_msg}',
+      })
+      return False
+    return True
+
+class CreateSimTarStep(base_step.BaseStep):
+
+  def __init__(self, **kwargs):
+    self._eurekaroot = kwargs['eureka_root']
+    base_step.BaseStep.__init__(self,
+                                name='tar-ing sim artifacts',
+                                halt_on_failure=True,
+                                **kwargs)
+
+
+  def run(self):
+    dockerignore = os.path.join(self._eurekaroot,
+        'vendor/x86_64_simulator/sirocco_x86_64/products',
+        'Dockerfile.dockerignore')
+
+    include_files = []
+    exclude_files = []
+    try:
+      with open(dockerignore) as f:
+        # Skip * on first line that excludes everything
+        for line in f.readlines()[1:]:
+          line = line.replace('\\', '') # Strip escaped + characters
+          if line.startswith('!'):
+            paths = self._eurekaroot.glob(line[1:].rstrip())
+            include_files.extend(
+                [str(path.relative_to(self._eurekaroot)) for path in paths])
+          else:
+            path = line.rstrip()
+            exclude_files.append(path)
+    except OSError:
+      # Ignore missing file here for tests
+      # tar would fail because the archive is empty
+      pass
+
+    tar_filename = '{}.tar.gz'.format(TARGET_PRODUCT)
+    command = ['tar']
+    command += ['-C', str(self._eurekaroot)]
+    command += [f'--exclude={file}' for file in exclude_files]
+    command += [
+      '-czf',
+      os.path.join(self.get_gcs_dir(), tar_filename),
+    ]
+    command += include_files
+
+    returncode, _, stderr = self.exec_subprocess(command)
+    if returncode != 0:
+      comment = f'tar-ing failed: {stderr}'
+      self.add_review({
+        'message': comment
+      })
+      return False
+
+    self.set_build_property('ota_files', [tar_filename])
+    return True
diff --git a/cq/scripts/slave/step/xml_check_step.py b/cq/scripts/slave/step/xml_check_step.py
new file mode 100644
index 0000000..78d33bd
--- /dev/null
+++ b/cq/scripts/slave/step/xml_check_step.py
@@ -0,0 +1,55 @@
+"""Build step class for validating XML files."""
+
+from __future__ import absolute_import
+import collections
+import os
+import re
+import xml.etree
+
+from helpers import git_utils
+from slave import base_step
+
+_XML_PARSE_ERROR_MSG = 'This XML file failed to parse: {}'
+_XML_ERROR_POSITION_PATTERN = r'line (?P<linenum>\d+), column \d+'
+
+
+class XmlCheckStep(base_step.BaseStep):
+  """Build step class for validating XML files."""
+
+  def __init__(self, **kwargs):
+    """Creates a XmlCheckStep instance.
+
+    Args:
+      **kwargs: Any additional args to pass to BaseStep.
+    """
+    base_step.BaseStep.__init__(self, name='xml check', **kwargs)
+
+  def _parse_error_to_comment(self, err):
+    comment = {'message': _XML_PARSE_ERROR_MSG.format(str(err))}
+    match = re.search(_XML_ERROR_POSITION_PATTERN, str(err))
+    if match:
+      comment['line'] = match.group('linenum')
+    return comment
+
+  def run(self):
+    """Attempts to parse all .xml files in the current commit.
+
+    Sets a build property "review" with the output if there are any errors.
+
+    Returns:
+      True iff there were no parsing errors.
+    """
+    comments = collections.defaultdict(list)
+    for xml_filename in self.changed_files(file_extensions=['.xml']):
+      with open(os.path.join(self.directory, xml_filename)) as xml_file:
+        try:
+          xml.etree.ElementTree.parse(xml_file)
+        except xml.etree.ElementTree.ParseError as err:
+          comments[xml_filename].append(self._parse_error_to_comment(err))
+
+    if comments:
+      self.add_review({'comments': comments})
+      return False
+
+    # No Errors
+    return True
diff --git a/cq/scripts/slave/test/__init__.py b/cq/scripts/slave/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cq/scripts/slave/test/__init__.py
diff --git a/cq/scripts/slave/test/test_generator.py b/cq/scripts/slave/test/test_generator.py
new file mode 100644
index 0000000..4187dfa
--- /dev/null
+++ b/cq/scripts/slave/test/test_generator.py
@@ -0,0 +1,250 @@
+"""Tools for generating custom recipe tests."""
+
+from __future__ import absolute_import
+import copy
+import re
+import sys
+
+import six
+
+DEFAULT_TEST_PROPERTIES = {
+    'revision_ref': 'refs/changes/45/12345/1',
+    'patch_branch': 'master',
+    'patch_project': 'fake/unittest',
+    'gerrit': 'eureka-internal',
+    'issue': 12345,
+    'patchset': 3,
+    'manifest_url': 'https://eureka-internal.googlesource.com/eureka/manifest',
+    'shallow_checkout': False,
+}
+
+# Mock manifest used by default in recipes_test.
+# Defined here to be used by custom test_generators as well.
+DEFAULT_MANIFEST_STRING = """
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest>
+  <remote fetch="https://chromium.googlesource.com/" name="chromium" review="https://chromium-review.googlesource.com/"/>
+  <remote fetch="https://eureka-internal.googlesource.com/" name="eureka" review="sso://eureka-internal"/>
+  <remote fetch="https://github.com/" name="github"/>
+
+  <default revision="master"/>
+
+  <project name="chromium/src" remote="eureka" revision="unfork_m43"/>
+  <project name="chromium/tools/depot_tools" remote="chromium"/>
+  <project name="chromiumos/chromite" path="chromite/chromite" remote="chromium"/>
+  <project name="chromiumos/third_party/autotest" path="autotest" remote="chromium"/>
+  <project name="continuous-tests" remote="eureka"/>
+  <project name="fake/unittest" remote="eureka"/>
+  <project name="fragglerock" remote="eureka"/>
+  <project name="prebuilt/toolchain" path="toolchain" remote="eureka"/>
+  <project name="google/sprockets" path="test/sprockets" remote="github"/>
+  <project name="test" remote="eureka"/>
+  <project name="chromecast/internal" path="chromium/src/chromecast/internal" remote="eureka"/>
+  <project name="chromecast/internal/voice_ui" path="chromium/src/chromecast/internal/chirp" remote="eureka"/>
+  <project name="vendor/amlogic" remote="eureka"/>
+  <project name="vendor/marvell" remote="eureka"/>
+</manifest>
+"""
+
+
+def repo_version_matcher():
+  return match('repo', 'version')
+
+
+def gclient_sync_matcher():
+  return match(
+      'luci-auth', 'context -disable-git-auth -service-account-json :gce -- ' +
+      'gclient sync --force --with_branch_heads ' +
+      '--delete_unversioned_trees --reset --jobs=[0-9]+')
+
+
+def match(tool, optional_arg_regex=''):
+  return '{} {}'.format(any_path_to(tool), optional_arg_regex)
+
+
+def any_path_to(tool):
+  return '(\/?[A-z]\/?)*\/?' + tool
+
+
+class TestGenerator(object):
+  """The state needed to run a single recipe test.
+
+  An instance of this class should represent a unique test for a build recipe,
+  allowing the recipe to provide fake data for one or more steps in order to
+  trigger a certain result.
+  """
+
+  def __init__(self, test_name, buildername, branch='master', properties=None):
+    """Instantiates a TestGenerator instance.
+
+    Args:
+      test_name: name for this test case. Unique within a recipe.
+      buildername: the name of the build used for the test. Must be valid within
+        the recipe this instance is used with.
+      branch: The manifest branch used to generate the manifest. Defaults to a
+        reasonable value.
+      properties: If set, these properties will be added to the dict returned by
+        get_properties, overriding the DEFAULT_TEST_PROPERTIES when duplicate
+        keys exist.
+    """
+    assert buildername
+    assert test_name
+    self._test_name = test_name
+    self._buildername = buildername
+    self._repo_branch = branch
+    self._custom_properties = properties
+
+    self._mock_files = {}
+    self._mock_executions = []
+
+  def add_mock_execution(self,
+                         execution_regex,
+                         retcode=0,
+                         stdout=None,
+                         stderr=None,
+                         reusable=True):
+    """Adds a mock execution result.
+
+    Args:
+      execution_regex: regex used to match an executed command.
+      retcode: return code to mock if the regex matches.
+      stdout: stdout to mock if the regex matches.
+      stderr: stderr to mock if the regex matches.
+    """
+    assert isinstance(execution_regex, six.string_types)
+    assert isinstance(retcode, int)
+    self._mock_executions.append({
+        'regex': execution_regex,
+        'retcode': retcode,
+        'used': False,
+        'stdout': stdout,
+        'stderr': stderr,
+        'reusable': reusable,
+    })
+
+  def exec_command(self, command):
+    """Mocks a command execution and returns exit values as appropriate.
+
+    Args:
+      command: the command to run, as a single string.
+
+    Returns:
+      The tuple (return code, stdout, stderr). If values are explicitly mocked,
+      those values will be inserted; otherwise, the default is (0, '', '').
+    """
+    assert isinstance(command, six.string_types)
+    for execution in self._mock_executions:
+      if execution['used'] and not execution['reusable']:
+        continue
+      if re.search(execution['regex'], command):
+        execution['used'] = True
+        stdout = execution['stdout'] or ''
+        stderr = execution['stderr'] or ''
+        return (execution['retcode'], stdout, stderr)
+    return (0, '', '')
+
+  def exec_non_blocking_command(self, command):
+    """Mocks a non blocking command execution and returns a mock subprocess.
+
+    Args:
+      command: the command to run, as a single string.
+
+    Returns:
+      A mock subprocess instance.
+    """
+    assert isinstance(command, six.string_types)
+
+    log_file_descriptor = 9999
+    log_file_path = '/tmpFolder/tmpPath'
+
+    class MockSubprocess(object):
+      returncode = None
+      pid = 123
+
+      def terminate(self, *args, **kwargs):  # pylint: disable=unused-argument,invalid-name
+        return 0
+
+      def poll(self, *args, **kwargs):  # pylint: disable=unused-argument,invalid-name
+        return 0
+
+      def kill(self):  # pylint: disable-unused-argument,invalid-name
+        return 0
+
+    return MockSubprocess(), log_file_descriptor, log_file_path
+
+  def add_mock_file(self, filename, file_contents):
+    """Adds a mock file result by filename.
+
+    A mock result will override file contents read using
+    BaseStep.exec_read_file.
+
+    Args:
+      filename: the path, relative or absolute, of the file to mock.
+      file_contents: the contents of the file to return when read.
+
+    Raises:
+      KeyError: if the filename was already mocked in this TestGenerator.
+    """
+    assert isinstance(filename, six.string_types)
+    assert isinstance(file_contents, six.string_types)
+    if filename in self._mock_files:
+      raise KeyError(filename)
+    self._mock_files[filename] = {'contents': file_contents, 'used': False}
+
+  def all_mocks_used(self):
+    """Returns whether or not all mocks were exercised for this test."""
+    for filename in self._mock_files:
+      mock = self._mock_files[filename]
+      if not mock['used']:
+        return False
+
+    for mock in self._mock_executions:
+      if not mock['used']:
+        return False
+
+    return True
+
+  def get_unused_mocks_message(self):
+    """Returns the list of mocks that were unused for this test."""
+    results = []
+    for filename in self._mock_files:
+      mock = self._mock_files[filename]
+      if not mock['used']:
+        results.append(str(mock))
+
+    for mock in self._mock_executions:
+      if not mock['used']:
+        results.append(str(mock))
+
+    if results:
+      return 'Unused mocks:\n{}'.format('\n'.join(results))
+    return None
+
+  def read_file(self, filename):
+    """Mocks reading file contents within a test context.
+
+    Args:
+      filename: the path, relative or absolute, of the file to read.
+
+    Returns:
+      The mocked file contents, or an empty string if not explicitly mocked.
+    """
+    if filename in self._mock_files:
+      self._mock_files[filename]['used'] = True
+      return self._mock_files[filename]['contents']
+    return ''
+
+  def get_test_name(self):
+    """Returns the test name represented by this instance."""
+    return self._test_name
+
+  def get_properties(self):
+    """Returns a set of build properties used for running this test."""
+    properties = copy.deepcopy(DEFAULT_TEST_PROPERTIES)
+    properties['manifest_branch'] = self._repo_branch
+    properties['buildername'] = self._buildername
+
+    if self._custom_properties:
+      properties.update(self._custom_properties)
+
+    return properties
diff --git a/cq/scripts/tools/__init__.py b/cq/scripts/tools/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cq/scripts/tools/__init__.py
diff --git a/cq/scripts/tools/fix-expectations b/cq/scripts/tools/fix-expectations
new file mode 100755
index 0000000..6b6c1df
--- /dev/null
+++ b/cq/scripts/tools/fix-expectations
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+import argparse
+import logging
+import os
+import re
+import typing
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("logfile", help="The log file from CQ to ingest")
+    parser.add_argument(
+        "-v", "--verbose",
+        action="store_true",
+        help="Add debug logging.")
+    args = parser.parse_args()
+
+    logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
+    with open(args.logfile, "r") as f:
+        _fix_expectation_files(f.read())
+    
+
+def _remove_ansi(line: str) -> str:
+    ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
+    return ansi_escape.sub('', line)
+
+def _fix_expectation_files(content: typing.Text) -> None:
+    filename = None
+    lines = []
+    filename_to_content = {}
+    for i, line in enumerate(content.split("\n")):
+        # Handle cases where we don't get a clean finishing line
+        # because of color codes.
+        line = _remove_ansi(line)
+        if line == "}\n":
+            lines.append(line)
+            content = "\n".join(lines)
+            if filename in filename_to_content:
+                if filename_to_content[filename] != content:
+                    _write_expectation(filename + ".1", 
+                        filename_to_content[filename])
+                    _write_expectation(filename + ".2", content)
+                    raise Exception("Whoa, got two conflicting contents.")
+            else:
+                filename_to_content[filename] = content
+            filename = None
+            lines = []
+            continue
+
+        if filename:
+            if not line:
+                lines.append("")
+                logging.debug(
+                    "Finished reading corrected version of %s "
+                    "at %d lines at line %d",
+                    filename, len(lines), i)
+                content = "\n".join(lines)
+
+                if filename in filename_to_content:
+                    if filename_to_content[filename] != content:
+                        _write_expectation(filename + ".1",
+                            filename_to_content[filename])
+                        _write_expectation(filename + ".2", content)
+                        raise Exception("Whoa, got two conflicting contents.")
+                else:
+                    filename_to_content[filename] = content
+                filename = None
+                lines = []
+            else:
+                lines.append(line)
+        else:
+            match = re.match(
+                "^Correct expectation file (?P<filename>.*):", line)
+            if match:
+                filename = match.group("filename")
+                logging.debug("Found corrected version of '%s'", filename)
+    for filename, file_content in filename_to_content.items():
+        _write_expectation(filename, file_content)
+
+def _write_expectation(filename: typing.Text, content: str) -> None:
+    fullpath = os.path.abspath(os.path.join(
+        os.path.dirname(__file__),
+        "..",
+        "slave",
+        "recipe_expectations",
+        filename,
+    ))
+    logging.info("Writing %s with %d lines", fullpath, len(content.split("\n")))
+    with open(fullpath, "w") as f:
+        f.write(content)
+
+if __name__ == "__main__":
+    main()
diff --git a/cq/scripts/tools/run_recipe.py b/cq/scripts/tools/run_recipe.py
new file mode 100755
index 0000000..7f2cbe3
--- /dev/null
+++ b/cq/scripts/tools/run_recipe.py
@@ -0,0 +1,910 @@
+#!/workspace/recipe_python_virtual_env/bin/python
+"""This script allows execution of a recipe without buildbot.
+
+Example:
+
+./run_recipe.py lint --properties-file - <<EOF
+{
+  'property1': 'value1',
+  'property2': 'value2',
+}
+EOF
+"""
+import argparse
+import contextlib
+import dataclasses
+import datetime
+import glob
+import io
+import json
+import logging
+import os
+import pathlib
+import pdb
+import resource
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+import traceback
+from typing import Dict, Union
+import urllib.parse
+
+# Allow imports from cq/scripts folder.
+sys.path.insert(
+    0, os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir)))
+# Allow imports from the root of continuous-tests
+sys.path.insert(
+    0,
+    os.path.realpath(
+        os.path.join(
+            os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
+            os.pardir)))
+# pylint: disable=wrong-import-position
+
+from helpers import data_manager
+from helpers import repo_utils
+from helpers import signal_utils
+from helpers import timeout
+from tools import subprocess42
+
+SUCCESS = 0
+FAILURE = 1
+
+USAGE = """
+%(prog)s <recipe_name> --properties-file <filename> [options]
+
+The properties file should contain a Python dictionary. If the filename
+"-" is used, then the dictionary is read from stdin, for example:
+
+%(prog)s recipe_name --properties-file - <<EOF
+{
+  'property1: 'value1',
+  'property2: 'value2',
+}
+EOF
+
+The --eureka-root specifies which working eureka repository to run the tests in.
+If unspecified, it will run in the current directory.
+"""
+
+# buildbot is reducing soft limit to 1024 and recipe inherits it from buildslave
+_RLIMIT_NOFILE_SOFT = 8192
+_RUNNING_STEP_NAME = None
+_REVIEW_MESSAGE_CHAR_LIMIT = 10000
+_TRUNCATED_MESSAGE = '...[truncated]'
+
+Properties = Dict['str', Union[str, int]]
+
+
+class RecipeRunnerError(Exception):
+  """Generic exception for recipe errors."""
+
+
+def get_tmp_dir():
+  """Returns the correct tmp dir."""
+  tmp_dir = os.environ.get('CQ_TEMP_DIR')
+  if not tmp_dir:
+    tmp_dir = ('/usr/local/google/tmp'
+               if os.path.isdir('/usr/local/google') else tempfile.gettempdir())
+  return tmp_dir
+
+
+def load_recipe_module(recipe):
+  """Returns a reference to the module for a given recipe.
+
+  Args:
+    recipe: name of the recipe (filename in scripts/slave/recipes).
+
+  Returns:
+    Handle to the module for the given recipe file.
+  """
+  sys_path_copy = sys.path[:]
+  sys.path.insert(
+      0, os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir)))
+  module = __import__('slave.recipe.' + recipe)
+  recipe_module = getattr(getattr(module, 'recipe'), recipe)
+  sys.path = sys_path_copy
+  return recipe_module
+
+
+def load_recipe(recipe, build_name: str, **kwargs):
+  """Loads a recipe from a name and properties structure.
+
+  Args:
+    recipe: name of the recipe (filename in scripts/slave/recipes).
+    build_name: name of the build to execute via this recipe.
+    **kwargs: additional named args to pass to the recipe
+
+  Returns:
+    A BaseRecipe instance corresponding to the given inputs.
+
+  Raises:
+    KeyError: if build_name not found in valid build names for the recipe.
+  """
+  recipe_module = load_recipe_module(recipe)
+  if build_name not in recipe_module.GetValidBuildNames():
+    raise KeyError('Build name "%s" not valid for recipe "%s"' %
+                   (build_name, recipe))
+  return recipe_module.CreateRecipe(build_name, **kwargs)
+
+
+def run_recipe(recipe,
+               properties: Properties,
+               eureka_root: str,
+               use_debugger: bool = False):
+  """Runs a recipe, mimicking BuildBot as needed.
+
+  Args:
+    recipe: name of the recipe (filename in scripts/slave/recipes).
+    properties: dictionary of properties for the given build.
+    eureka_root: the root directory to run the commands in.
+    use_debugger: Boolean param to pass to run_steps().
+
+  Returns:
+    one of SUCCESS or FAILURE.
+  """
+  annotator = Annotator()
+  gcs_dir = properties.get('artifacts_dir')
+  executor = SubprocessExecutor(gcs_dir)
+  assert 'buildername' in properties
+  build_name = properties['buildername']
+  eureka_root_abs = os.path.abspath(eureka_root)
+
+  manifest_output_path = (
+      pathlib.Path(executor.get_gcs_dir()) / repo_utils.MANIFEST_OVERRIDE_XML)
+  manifest_output_path.write_text(
+      repo_utils.get_manifest(executor, cwd=eureka_root_abs, quiet=True))
+  properties['manifest_override_path'] = str(manifest_output_path)
+  annotator.set_build_property(
+      'manifest_override_path', str(manifest_output_path))
+
+  dm = data_manager.DataManager(executor, properties)
+  dm.add_data('recipe_start_time', datetime.datetime.now())
+  recipe = load_recipe(
+      recipe,
+      build_name,
+      properties=properties,
+      annotator=annotator,
+      executor=executor,
+      data_manager=dm,
+      eureka_root=eureka_root_abs)
+
+  logging.info('Running setup steps.')
+  result = run_steps(recipe.get_setup_steps(), eureka_root_abs, annotator)
+
+  if result == SUCCESS:
+    logging.info('Running main recipe steps.')
+    result = run_steps(
+        recipe.get_steps(),
+        eureka_root_abs,
+        annotator,
+        use_debugger=use_debugger)
+
+  logging.info('Running teardown steps.')
+  run_steps(recipe.get_teardown_steps(), eureka_root_abs, annotator)
+
+  write_result_file(
+      annotator.message,
+      annotator.file_comments,
+      annotator.output_properties,
+  )
+
+  return result
+
+
+def run_steps(steps, root_dir, annotator, use_debugger=False):
+  """Runs a list of steps.
+
+  Args:
+    steps: A list of steps to run.
+    root_dir: The root directory to reset to prior to each step.
+    annotator: An instance of an Annotator to use to run each step.
+    use_debugger: If True will re-raise exception
+
+  Returns:
+    one of SUCCESS or FAILURE.
+  """
+  global _RUNNING_STEP_NAME
+
+  result = SUCCESS
+  for step in steps:
+    # reset cwd to root before each step, allowing steps to change cwd
+    os.chdir(root_dir)
+    logging.info('Running step: %s', step.get_name())
+    exception = None
+    step_result = FAILURE
+    try:
+      _RUNNING_STEP_NAME = step.get_name()
+      step_result = annotator.run_step(step)
+    except Exception:  # pylint: disable=broad-except
+      logging.exception('Encountered an unexpected error while running %s',
+                        step.get_name())
+      exception = traceback.format_exc()
+    finally:
+      _RUNNING_STEP_NAME = None
+      if exception:
+        if use_debugger:
+          raise  # pylint: disable=misplaced-bare-raise
+
+        return FAILURE  # pylint: disable=lost-exception
+
+    result = max(result, step_result)
+
+    if result != SUCCESS and step.halt_on_failure():
+      logging.info('Halting on failure from step %s', step.get_name())
+      break
+
+  return result
+
+
+class ExecutorInvalidKeywordArgsError(Exception):
+  """SubprocessExecutor expects a single command list."""
+
+
+class ExecutorInvalidArgsError(Exception):
+  """SubprocessExecutor expects a single command list."""
+
+
+class SubprocessExecutor(object):
+  """Executes subprocess commands."""
+
+  def __init__(self, gcs_dir):
+    """Constructor for SubprocessExecutor. Creates temporary directories."""
+    tempdir = get_tmp_dir()
+    self._tempdir = tempfile.mkdtemp(dir=tempdir)
+    self._gcs_dir = gcs_dir
+    self._cq_root = os.path.abspath(
+        os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
+    self._blocking_subprocesses = []
+
+  def get_cq_root(self):
+    """Returns the absolute path to the root of the CQ src."""
+    return self._cq_root
+
+  def get_tmp_dir(self):
+    """Returns a tempdir available to the executor."""
+    return self._tempdir
+
+  def get_gcs_dir(self):
+    """This directory will be uploaded to GCS when a recipe finishes."""
+    return self._gcs_dir
+
+  def get_new_tmp_file(self):
+    """Creates a new temporary file.
+
+    Returns:
+      A tuple of newly created temporary file descriptor and absolute path.
+    """
+    return tempfile.mkstemp(dir=self.get_tmp_dir(), text=True)
+
+  def write_temp_file(self, contents: str):
+    """Writes a temporary file.
+
+    Args:
+      contents: Text string to write
+
+    Returns:
+      The name of the file.
+    """
+    tmp_file, tmp_file_path = self.get_new_tmp_file()
+    os.write(tmp_file, contents.encode('utf-8'))
+    os.close(tmp_file)
+    return tmp_file_path
+
+  def get_blocking_subprocesses(self):
+    """Returns all the blocking subproccesses this executor has started."""
+    return self._blocking_subprocesses
+
+  def read_file(self, file_path):
+    """Reads a file by path and returns the contents.
+
+    No sanity checking or validation is performed by this method.
+
+    Args:
+      file_path: path to a file, either relative from cwd or absolute.
+
+    Returns:
+      The contents of the file loaded from file_path.
+    """
+    with open(file_path) as f:
+      return f.read()
+
+  def glob_files(self, glob_expr):
+    """Performs a file glob and returns the list of files.
+
+    Args:
+      glob_expr: the file glob expression to search for.
+
+    Returns:
+      A list, the output of glob.glob(glob_expr).
+    """
+    assert isinstance(glob_expr, str)
+    return glob.glob(glob_expr)
+
+  def exec_subprocess(self,
+                      command,
+                      check_output=False,
+                      cwd=None,
+                      *args,
+                      **kwargs):  # pylint: disable=keyword-arg-before-vararg
+    """Execute a subprocess.
+
+    Args:
+      command: The command to execute.
+      check_output: Whether or not to raise a CalledProcessError on non-zero
+        return code.
+      cwd: The current working directory to use for the subprocess.
+      *args: passed to self._executor.subprocess
+      **kwargs: passed to self._executor.subprocess  This should be used
+        sparingly at the recipe level because none of the information about the
+        subprocess is captured in any logs.
+
+    Returns:
+      Just stdout, if check_output=True, otherwise a tuple of
+      (returncode, stdout, stderr).
+    """
+    returncode, stdout, stderr = self.subprocess(
+        command, *args, cwd=cwd, **kwargs)
+    if check_output:
+      if returncode != 0:
+        raise subprocess.CalledProcessError(
+            returncode, cmd=command, output=stdout + '\nstderr:\n' + stderr)
+      return stdout
+    return (returncode, stdout, stderr)
+
+  def subprocess(self, *args, **kwargs):
+    """Executes a subprocess using subprocess.Popen.
+
+    Args:
+      *args: passed to subprocess.Popen
+      **kwargs: passed to subprocess.Popen; optionally, quiet=True can be passed
+        as a kwarg, in which case stdout/stderr from the subprocess will not be
+        written to sys. This kwarg will not get passed to subprocess.Popen.
+
+    Returns:
+      Tuple of (return code, full stdout output, full stderr output)
+
+    Raises:
+      ExecutorInvalidArgsError: if |args| is not a single command list.
+    """
+    if len(args) != 1 or not isinstance(args[0], list):
+      raise ExecutorInvalidArgsError(
+          'Expected a tuple with a single list element, found args={}'.format(
+              args))
+
+    stdin = None
+    stdout = subprocess.PIPE
+    stderr = subprocess.PIPE
+    if 'stdin' in kwargs:
+      stdin = kwargs['stdin']
+      del kwargs['stdin']
+    if 'stdout' in kwargs:
+      stdout = kwargs['stdout']
+      del kwargs['stdout']
+    if 'stderr' in kwargs:
+      stderr = kwargs['stderr']
+      del kwargs['stderr']
+
+    sp, quiet = (self.start_subprocess(stdin, stdout, stderr, *args, **kwargs))
+    self._blocking_subprocesses.append(sp)
+    stdout, stderr = self.wait_for_subprocess_completion(sp, quiet)
+    logging.info('[%s] Completed command "%s" with return code %d',
+                  time.strftime('%Y-%m-%d %H:%M:%S %Z'),
+                  ' '.join(map(str, args[0])), sp.returncode)
+    return (sp.returncode, stdout, stderr)
+
+  def subprocess_chain(self, subprocess_chain):
+    """Executs a chain of subprocesses, piping output between processes.
+
+    Args:
+      subprocess_chain: A list of subprocesses, where each item is a tuple of
+        (command, **kwargs) to pass to subprocess.Popen. If command is empty, it
+        must be []. If **kwargs is empty, it must be {}.
+                        e.g. [(['ls', '-al'], {'cwd':'path/to/foo'}), ([], {})]
+
+    Returns:
+      Tuple of (return code, full stdout output, full stderr output)
+    """
+    assert isinstance(subprocess_chain, list)
+
+    logging.info('Starting command pipeline.')
+    prev_process = None
+    for command, kwargs in subprocess_chain:
+      assert isinstance(command, list)
+      assert isinstance(kwargs, dict)
+      if prev_process:
+        sp, quiet = self.start_subprocess(prev_process.stdout, subprocess.PIPE,
+                                          subprocess.PIPE, command, **kwargs)
+        prev_process.stdout.close()  # Allow prev_process to receive a SIGPIPE
+        prev_process = sp
+      else:
+        # First process starts without piping in any stdin
+        prev_process, quiet = (
+            self.start_subprocess(None, subprocess.PIPE, subprocess.PIPE,
+                                  command, **kwargs))
+      self._blocking_subprocesses.append(prev_process)
+
+    stdout, stderr = self.wait_for_subprocess_completion(prev_process, quiet)
+    logging.info('Command pipeline finished with returncode %d',
+                 prev_process.returncode)
+    return (prev_process.returncode, stdout, stderr)
+
+  def non_blocking_subprocess(self, *args, **kwargs):
+    """Starts a non blocking subprocess using subprocess.Popen.
+
+    The new subprocess is detached from parent process and created under a new
+    process group.
+
+    Args:
+      *args: passed to subprocess.Popen
+      **kwargs: passed to subprocess.Popen.
+
+    Returns:
+      A tuple of newly created subprocess, log file descriptor and log file
+      path.
+    """
+    kwargs['detached'] = True
+    file_descriptor, file_path = self.get_new_tmp_file()
+    sp, _ = self.start_subprocess(None, file_descriptor, file_descriptor, *args,
+                                  **kwargs)
+    logging.info('Started command %s, pid = %d', args, sp.pid)
+    return sp, file_descriptor, file_path
+
+  def start_subprocess(self, stdin, stdout, stderr, *args, **kwargs):
+    """Executes a subprocess using subprocess.Popen.
+
+    Args:
+      stdin: A file stream, subprocess.PIPE, or None to read stdin from.
+      stdout: A file stream or subprocess.PIPE to redirect stdout.
+      stderr: A file stream or subprocess.PIPE to redirect stderr.
+      *args: passed to subprocess.Popen
+      **kwargs: passed to subprocess.Popen; optionally, quiet=True can be passed
+        as a kwarg, in which case stdout/stderr from the subprocess will not be
+        written to file. This kwarg will not get passed to subprocess.Popen.
+                text: If stdout and stderr should be treated as text, encoded in
+                  the default encoding for the platform.
+
+    Returns:
+      Tuple of (subprocess, boolean indicating if subprocess output needs to be
+                saved.)
+
+    Raises:
+      ExecutorInvalidArgsError: if |args| is not a single command list.
+      ExecutorInvalidKeywordArgsError: if stdin, stdout, stderr in |kwargs|.
+    """
+    for kwarg in ['stdin', 'stdout', 'stderr']:
+      if kwarg in kwargs:
+        raise ExecutorInvalidKeywordArgsError(
+            '{} cannot be a kwarg: kwargs={}'.format(kwarg, kwargs))
+    if len(args) != 1 or not isinstance(args[0], list):
+      raise ExecutorInvalidArgsError(
+          'Expected a tuple with a single list element, found args={}'.format(
+              args))
+
+    if 'cwd' in kwargs:
+      if kwargs['cwd'] is None:
+        cwd_message = 'with cwd=None, using {}'.format(os.path.abspath('.'))
+      else:
+        cwd_message = 'with specified cwd of "{}"'.format(kwargs['cwd'])
+    else:
+      cwd_message = 'with default cwd of "{}"'.format(os.path.abspath('.'))
+
+    text = kwargs.get('text', True)
+    quiet = not text
+    if 'quiet' in kwargs:
+      quiet = kwargs['quiet']
+      del kwargs['quiet']
+
+    logging.info('[%s] Running command: "%s" %s, with env: %s',
+                time.strftime('%Y-%m-%d %H:%M:%S %Z'),
+                ' '.join(map(str, args[0])), cwd_message,
+                kwargs.get('env', {}))
+
+    if not text and not quiet:
+      raise ValueError('quiet and text cannot both be False')
+    # Add any extra args from 'env' to a copy of os.environ
+    extra_env = kwargs.pop('env', {})
+    env = os.environ.copy()
+    if extra_env:
+      env.update(extra_env)
+    if 'TMPDIR' not in env:
+      env['TMPDIR'] = self.get_tmp_dir()
+    sp = subprocess42.Popen(
+        stdin=stdin,
+        stdout=stdout,
+        stderr=stderr,
+        env=env,
+        text=text,
+        *args,
+        **kwargs)
+    return (sp, quiet)
+
+  def wait_for_subprocess_completion(self, sp, quiet):
+    """Waits till subprocess is completed.
+
+    Args:
+      sp: A file stream or subprocess.PIPE to redirect stdout.
+      quiet: If True stdout/stderr from the subprocess will not be written to
+        file/console.
+
+    Returns:
+      Tuple of strings (stdout, stderr)
+    """
+    stdout_array = []
+    stderr_array = []
+
+    for pipe, data in sp.yield_any():
+      if pipe == 'stdout':
+        if not quiet:
+          sys.stdout.write(data)
+          sys.stdout.flush()
+        stdout_array.append(data)
+      else:
+        if not quiet:
+          sys.stderr.write(data)
+          sys.stderr.flush()
+        stderr_array.append(data)
+
+    return ''.join(stdout_array), ''.join(stderr_array)
+
+
+def print_command(command):
+  return sys.__stdout__.write(json.dumps(command) + '\n')
+
+
+class JsonIOWrapper(io.TextIOBase):
+
+  def __init__(self, channel):
+    super().__init__()
+    self._channel = channel
+    self._line = ''
+
+  def flush(self) -> None:
+    if self._line:
+      self._write_line()
+
+  def write(self, s: str) -> int:
+    self._line += s
+    if self._line.endswith('\n') or len(self._line) > 1000:
+      return self._write_line()
+    return 0
+
+  def _write_line(self) -> int:
+    log_command = {
+        'channel': self._channel,
+        'text': self._line,
+    }
+    self._line = ''
+    if _RUNNING_STEP_NAME:
+      log_command['step_name'] = _RUNNING_STEP_NAME
+    return print_command({
+        'log': log_command,
+    })
+
+
+@dataclasses.dataclass(frozen=True)
+class FileCommentRange:
+  start_line: int
+  start_character: int
+  end_line: int
+  end_character: int
+
+
+@dataclasses.dataclass(frozen=True)
+class FileComment:
+  file_path: str
+  location: Union[int, FileCommentRange, None]
+  message: str
+
+
+class Annotator(object):
+  """Prints annotation commands to be interpreted by the buildbot master."""
+
+  def __init__(self):
+    self._output_properties = {}
+    self._messages = []
+    self._file_comments = []
+
+  @property
+  def output_properties(self):
+    return self._output_properties
+
+  @property
+  def message(self):
+    return truncate('\n\n'.join(self._messages), 10000)
+
+  @property
+  def file_comments(self):
+    return self._file_comments[:100]
+
+  def run_step(self, step):
+    """Runs a build step, printing annotations as necessary.
+
+    Arguments:
+      step: BaseStep instance to be executed.
+
+    Returns:
+      SUCCESS if the step runs to completion, FAILURE otherwise.
+    """
+    print_command({
+        'start_step': {
+            'step_name': step.get_name(),
+        },
+    })
+
+    return_value = SUCCESS
+    result = False
+
+    try:
+      timeout_sec = step.get_timeout()
+      with signal_utils.sigterm_handler():
+        with timeout.timeout(timeout_secs=timeout_sec):
+          result = step.run()
+      assert isinstance(result, bool)
+    except timeout.TimeoutException:
+      result = step.result_on_timeout()
+      logging.error(
+          'Error: Step timed out. The current step timeout '
+          'threshold is %s seconds', timeout_sec)
+      step.kill_all_blocking_subprocesses()
+    except signal_utils.SigtermException:
+      logging.error('Error: Received SIGTERM. If this was run by Buildbot, '
+                    'that means a step timed out because no logs was '
+                    'produced for a specified period of time. We should stop '
+                    'and try to clean up everything before Buildbot sends '
+                    'a SIGKILL.')
+      step.set_halt_on_failure()
+      step.kill_all_blocking_subprocesses()
+    except Exception:  # pylint: disable=broad-except
+      logging.exception('Step %s finished with an exception.', step.get_name())
+      raise
+    finally:
+      if not result:
+        return_value = FAILURE
+      step.kill_all_non_blocking_subprocesses()
+
+    print_command({
+        'finish_step': {
+            'step_name': step.get_name(),
+            'outcome': 'FAILED' if return_value == FAILURE else 'PASSED'
+        },
+    })
+
+    return return_value
+
+  def set_build_property(self, name, value):
+    """Communicates a new build property to the buildbot master.
+
+    Args:
+      name: property name (must be a string)
+      value: property value (must be JSON-serializable)
+    """
+    self._output_properties[name] = value
+
+  def add_message(self, message):
+    self._messages.append(message)
+
+  def add_comments(self, comments):
+    for filename, file_comments in comments.items():
+      if len(filename) > 200:
+        logging.warning('Skipping file comment because path is too long')
+      else:
+        assert isinstance(file_comments, list)
+        for comment in file_comments:
+          if 'line' in comment:
+            location = comment['line']
+            assert isinstance(location, int) and location > 0
+          elif 'range' in comment:
+            range_data = comment['range']
+            fields = [
+                ('start_line', 1),
+                ('start_character', 0),
+                ('end_line', 1),
+                ('end_character', 0)]
+            for field, minimum_value in fields:
+              assert field in range_data
+              assert isinstance(range_data[field], int)
+              assert range_data[field] >= minimum_value
+            location = FileCommentRange(
+                start_line=range_data['start_line'],
+                start_character=range_data['start_character'],
+                end_line=range_data['end_line'],
+                end_character=range_data['end_character'],
+            )
+          else:
+            location = None
+          self._file_comments.append(FileComment(
+              file_path=filename,
+              location=location,
+              message=truncate(comment['message'], 1000),
+          ))
+
+
+def truncate(s, max_length) -> str:
+  if len(s) > max_length:
+    return s[:(max_length - len(_TRUNCATED_MESSAGE))] + _TRUNCATED_MESSAGE
+  return s
+
+
+def postmortem(type_, value, tb):
+  """A sys.excepthook for handling exceptions with pdb."""
+  traceback.print_exception(type_, value, tb)
+  pdb.pm()
+
+
+def load_recipe_properties(parsed_args) -> Properties:
+  """Loads recipe properties from file or from params JSON."""
+  if parsed_args.properties_file:
+    filename = parsed_args.properties_file
+    properties_file = sys.stdin if filename == '-' else open(filename)
+    properties = json.loads(properties_file.read())
+    assert isinstance(properties, dict)
+  elif parsed_args.properties:
+    properties = json.loads(parsed_args.properties)
+    assert isinstance(properties, dict)
+  else:
+    raise RecipeRunnerError('properties are required')
+
+  return properties
+
+
+def amend_recipe_properties(properties):
+  """Set defaults and convert special values in recipe properties."""
+  properties = update_recipe_properties_from_environment(properties)
+
+  manifest_url = properties.get('manifest_url')
+  if manifest_url:
+    properties['repo_alias'] = repo_utils.manifest_alias(manifest_url)
+  return properties
+
+
+def get_branch_root(properties) -> str:
+  """Returns workspace_checkout_path from the properties if it exists."""
+
+  branch_root = properties.get('workspace_checkout_path')
+  if not branch_root:
+    raise ValueError('workspace_checkout_path is a required recipe parameter.')
+  if not os.path.exists(branch_root):
+    logging.info('Creating directory %s', branch_root)
+    os.makedirs(branch_root)
+  return branch_root
+
+
+def write_result_file(message, file_comments, output_properties):
+  result_file = pathlib.Path('/workspace/output/result.json')
+  result_file.write_text(json.dumps({
+      'message': message,
+      'file_comments': [convert_file_comment(c) for c in file_comments],
+      'output_properties': output_properties,
+  }))
+
+
+def convert_file_comment(file_comment):
+  comment = {
+      'file_path': file_comment.file_path,
+      'message': file_comment.message,
+  }
+  if isinstance(file_comment.location, int):
+    comment['line'] = file_comment.location
+  elif isinstance(file_comment.location, FileCommentRange):
+    comment['range'] = dataclasses.asdict(file_comment.location)
+  return comment
+
+
+def get_build_system(austin_build_system):
+  return 'cq' if austin_build_system == 'presubmit' else 'catabuilder'
+
+
+def get_gob_host_id(host_url: str) -> str:
+  """Returns the ID of the GoB host. Derived from host_url."""
+  hostname = get_hostname(host_url)
+  if hostname:
+    parts = hostname.split('.')
+    if len(parts) == 3 and hostname.endswith('.googlesource.com'):
+      return parts[0]
+    if len(parts) == 5 and hostname.endswith('.git.corp.google.com'):
+      return parts[0]
+  raise ValueError('Unable to extract GoB host ID from host URL.')
+
+
+def get_hostname(host_url: str) -> str:
+  return urllib.parse.urlparse(host_url).hostname
+
+
+def get_depends_on(patchset_data):
+  return {
+      'change_number': int(patchset_data['change_number']),
+      'gob_host': get_gob_host_id(patchset_data['host_url']),
+      'patchset_number': int(patchset_data['patch_number']),
+      'project': patchset_data['project'],
+      'branch': patchset_data['target_branch'],
+  }
+
+
+def get_patchset_properties():
+  patchset_data = json.loads(os.environ['AUSTIN_PATCHSETS'])
+  if not patchset_data:
+    return {}
+  main_patch = patchset_data[-1]
+  return {
+      'depends_on': [get_depends_on(p) for p in patchset_data[:-1]],
+      'gerrit': get_gob_host_id(main_patch['host_url']),
+      'issue': int(main_patch['change_number']),
+      'patch_project': main_patch['project'],
+      'patchset': int(main_patch['patch_number']),
+  }
+
+
+def update_recipe_properties_from_environment(recipe_properties):
+  return {
+      **recipe_properties,
+      'artifacts_dir': os.environ['AUSTIN_OUTPUT_ARTIFACT_PATH'],
+      'basil_build_id': os.environ['AUSTIN_BUILD_ID'],
+      'build_env': os.environ['AUSTIN_BUILD_ENVIRONMENT'],
+      'build_system': get_build_system(os.environ['AUSTIN_BUILD_SYSTEM']),
+      'buildset': os.environ['AUSTIN_BUILDSET_ID'],
+      'container_push_url': os.environ['AUSTIN_CONTAINER_PUSH_URL'],
+      'manifest_branch': os.environ['AUSTIN_MANIFEST_BRANCH'],
+      'manifest_url': os.environ['AUSTIN_MANIFEST_GIT_REPO_URL'],
+      'workspace_checkout_path': os.environ['AUSTIN_CHECKOUT_PATH'],
+      'rbe_instance': os.environ['AUSTIN_RBE_INSTANCE'],
+      **get_patchset_properties(),
+  }
+
+
+def main(args):
+  """Gets the recipe name and properties and runs an annotated run."""
+  print_command({
+      'enable_steps': {},
+  })
+  with contextlib.redirect_stdout(JsonIOWrapper('STDOUT')):
+    with contextlib.redirect_stderr(JsonIOWrapper('STDERR')):
+      logging.getLogger().setLevel(level=logging.INFO)
+      try:
+        return main_logged(args)
+      except BaseException:
+        logging.exception('An unexpected error occurred')
+        return 1
+
+
+def main_logged(args):
+  # Increase NOFILE soft limit to allow bigger builds, see b/140431372
+  _, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
+  resource.setrlimit(resource.RLIMIT_NOFILE,
+                     (min(hard_limit, _RLIMIT_NOFILE_SOFT), hard_limit))
+
+  parser = argparse.ArgumentParser(usage=USAGE)
+  parser.add_argument('recipe')
+  parser.add_argument(
+      '--pdb',
+      action='store_true',
+      help='Drop into a pdb session on unhandled exception')
+  parser.add_argument('--properties-file')
+  parser.add_argument('--properties')
+  parsed_args = parser.parse_args(args)
+
+  if parsed_args.pdb:
+    sys.excepthook = postmortem
+
+  properties = load_recipe_properties(parsed_args)
+  logging.info(
+      'Properties: %s', json.dumps(properties, indent=2, sort_keys=True))
+  properties = amend_recipe_properties(properties)
+  logging.info(
+      'Amended properties: %s',
+      json.dumps(properties, indent=2, sort_keys=True),
+  )
+
+  branch_root = get_branch_root(properties)
+
+  return run_recipe(
+      recipe=parsed_args.recipe,
+      properties=properties,
+      eureka_root=branch_root,
+      use_debugger=parsed_args.pdb)
+
+
+if __name__ == '__main__':
+  sys.exit(main(sys.argv[1:]))
diff --git a/cq/scripts/tools/subprocess42.py b/cq/scripts/tools/subprocess42.py
new file mode 100644
index 0000000..dfb279a
--- /dev/null
+++ b/cq/scripts/tools/subprocess42.py
@@ -0,0 +1,678 @@
+# Copyright 2013 The LUCI Authors. All rights reserved.
+# Use of this source code is governed under the Apache License, Version 2.0
+# that can be found in the LICENSE file.
+
+"""subprocess42 is the answer to life the universe and everything.
+
+It has the particularity of having a Popen implementation that can yield output
+as it is produced while implementing a timeout and NOT requiring the use of
+worker threads.
+
+Example:
+  Wait for a child process with a timeout, send SIGTERM, wait a grace period
+  then send SIGKILL:
+
+    def wait_terminate_then_kill(proc, timeout, grace):
+      try:
+        return proc.wait(timeout)
+      except subprocess42.TimeoutExpired:
+        proc.terminate()
+        try:
+          return proc.wait(grace)
+        except subprocess42.TimeoutExpired:
+          proc.kill()
+        return proc.wait()
+
+
+TODO(maruel): Add VOID support like subprocess2.
+"""
+import collections
+import contextlib
+import errno
+import locale
+import os
+import signal
+import sys
+import threading
+import time
+
+import subprocess
+
+from cq.scripts.helpers import timeout as time_out
+
+
+# Default maxsize argument.
+MAX_SIZE = 16384
+
+
+# Set to True when inhibit_crash_dump() has been called.
+_OS_ERROR_REPORTING_INHIBITED = False
+
+
+# subprocess.mswindows is an internal-only API. It was moved in Python3.
+mswindows = getattr(
+    subprocess, "mswindows", getattr(subprocess, "_mswindows", False))
+if mswindows:
+  # pylint: disable=wrong-import-position
+  import ctypes  # pylint: disable=wrong-import-order
+  import msvcrt  # pylint: disable=wrong-import-order
+  from ctypes import wintypes  # pylint: disable=wrong-import-order
+  from ctypes import windll  # pylint: disable=wrong-import-order
+
+
+  # Which to be received depends on how this process was called and outside the
+  # control of this script. See Popen docstring for more details.
+  STOP_SIGNALS = (signal.SIGBREAK, signal.SIGTERM)
+
+
+  def ReadFile(handle, desired_bytes):
+    """Calls kernel32.ReadFile()."""
+    c_read = wintypes.DWORD()
+    buff = wintypes.create_string_buffer(desired_bytes+1)
+    windll.kernel32.ReadFile(
+        handle, buff, desired_bytes, wintypes.byref(c_read), None)
+    # NULL terminate it.
+    buff[c_read.value] = '\x00'
+    return wintypes.GetLastError(), buff.value
+
+  def PeekNamedPipe(handle):
+    """Calls kernel32.PeekNamedPipe(). Simplified version."""
+    c_avail = wintypes.DWORD()
+    c_message = wintypes.DWORD()
+    success = windll.kernel32.PeekNamedPipe(
+        handle, None, 0, None, wintypes.byref(c_avail),
+        wintypes.byref(c_message))
+    if not success:
+      raise OSError(wintypes.GetLastError())
+    return c_avail.value
+
+  def recv_multi_impl(conns, maxsize, timeout):
+    """Reads from the first available pipe.
+
+    It will immediately return on a closed connection, independent of timeout.
+
+    Arguments:
+    - maxsize: Maximum number of bytes to return. Defaults to MAX_SIZE.
+    - timeout: If None, it is blocking. If 0 or above, will return None if no
+          data is available within |timeout| seconds.
+
+    Returns:
+      tuple(int(index), str(data), bool(closed)).
+    """
+    assert conns
+    assert timeout is None or isinstance(timeout, (int, float)), timeout
+    maxsize = max(maxsize or MAX_SIZE, 1)
+
+    # TODO(maruel): Use WaitForMultipleObjects(). Python creates anonymous pipes
+    # for proc.stdout and proc.stderr but they are implemented as named pipes on
+    # Windows. Since named pipes are not waitable object, they can't be passed
+    # as-is to WFMO(). So this means N times CreateEvent(), N times ReadFile()
+    # and finally WFMO(). This requires caching the events handles in the Popen
+    # object and remembering the pending ReadFile() calls. This will require
+    # some re-architecture to store the relevant event handle and OVERLAPPEDIO
+    # object in Popen or the file object.
+    start = time.time()
+    handles = [
+      (i, msvcrt.get_osfhandle(c.fileno())) for i, c in enumerate(conns)
+    ]
+    while True:
+      for index, handle in handles:
+        try:
+          avail = min(PeekNamedPipe(handle), maxsize)
+          if avail:
+            return index, ReadFile(handle, avail)[1], False
+        except OSError:
+          # The pipe closed.
+          return index, None, True
+
+      if timeout is not None and (time.time() - start) >= timeout:
+        return None, None, False
+      # Polling rocks.
+      time.sleep(0.001)
+
+else:
+  import fcntl  # pylint: disable=wrong-import-order,wrong-import-position
+  import select  # pylint: disable=wrong-import-order,wrong-import-position
+
+
+  # Signals that mean this process should exit quickly.
+  STOP_SIGNALS = (signal.SIGINT, signal.SIGTERM)
+
+
+  def recv_multi_impl(conns, maxsize, timeout):
+    """Reads from the first available pipe.
+
+    It will immediately return on a closed connection, independent of timeout.
+
+    Arguments:
+    - maxsize: Maximum number of bytes to return. Defaults to MAX_SIZE.
+    - timeout: If None, it is blocking. If 0 or above, will return None if no
+          data is available within |timeout| seconds.
+
+    Returns:
+      tuple(int(index), str(data), bool(closed)).
+    """
+    assert conns
+    assert timeout is None or isinstance(timeout, (int, float)), timeout
+    maxsize = max(maxsize or MAX_SIZE, 1)
+
+    # select(timeout=0) will block, it has to be a value > 0.
+    if timeout == 0:
+      timeout = 0.001
+    try:
+      r, _, _ = select.select(conns, [], [], timeout)
+    except select.error:
+      r = None
+    if not r:
+      return None, None, False
+
+    conn = r[0]
+    # Temporarily make it non-blocking.
+    # TODO(maruel): This is not very efficient when the caller is doing this in
+    # a loop. Add a mechanism to have the caller handle this.
+    flags = fcntl.fcntl(conn, fcntl.F_GETFL)
+    if not conn.closed:
+      # pylint: disable=E1101
+      fcntl.fcntl(conn, fcntl.F_SETFL, flags | os.O_NONBLOCK)
+    try:
+      try:
+        data = conn.read(maxsize)
+      except IOError as e:
+        # On posix, this means the read would block.
+        if e.errno == errno.EAGAIN:
+          return conns.index(conn), None, False
+        raise e
+
+      if not data:
+        # On posix, this means the channel closed.
+        return conns.index(conn), None, True
+
+      return conns.index(conn), data, False
+    finally:
+      if not conn.closed:
+        fcntl.fcntl(conn, fcntl.F_SETFL, flags)
+
+
+class TimeoutExpired(time_out.TimeoutException):
+  """Compatible with python3 subprocess."""
+  def __init__(self, cmd, timeout, output=None, stderr=None):
+    self.cmd = cmd
+    self.timeout = timeout
+    self.output = output
+    # Non-standard:
+    self.stderr = stderr
+    super(TimeoutExpired, self).__init__(str(self))
+
+  def __str__(self):
+    return "Command '%s' timed out after %s seconds" % (self.cmd, self.timeout)
+
+
+class Popen(subprocess.Popen):
+  """Adds timeout support on stdout and stderr.
+
+  Inspired by
+  http://code.activestate.com/recipes/440554-module-to-allow-asynchronous-subprocess-use-on-win/
+
+  Unlike subprocess, yield_any(), recv_*(), communicate() will close stdout and
+  stderr once the child process closes them, after all the data is read.
+
+  Mutated behavior:
+  - args: transparently encode('utf-8') any unicode items.
+  - cwd: transparently encode('utf-8') if unicode.
+  - env: transparently encode('utf-8') any unicode keys or values.
+
+  Additional arguments:
+  - detached: If True, the process is created in a new process group. On
+    Windows, use CREATE_NEW_PROCESS_GROUP. On posix, use os.setpgid(0, 0).
+
+  Additional members:
+  - start: timestamp when this process started.
+  - end: timestamp when this process exited, as seen by this process.
+  - detached: If True, the child process was started as a detached process.
+  - gid: process group id, if any.
+  - duration: time in seconds the process lasted.
+
+  Additional methods:
+  - yield_any(): yields output until the process terminates.
+  - recv_any(): reads from stdout and/or stderr with optional timeout.
+  - recv_out() & recv_err(): specialized version of recv_any().
+  """
+  # subprocess.Popen.__init__() is not threadsafe; there is a race between
+  # creating the exec-error pipe for the child and setting it to CLOEXEC during
+  # which another thread can fork and cause the pipe to be inherited by its
+  # descendents, which will cause the current Popen to hang until all those
+  # descendents exit. Protect this with a lock so that only one fork/exec can
+  # happen at a time.
+  popen_lock = threading.Lock()
+
+  def __init__(self, args, text=False, **kwargs):
+    assert 'creationflags' not in kwargs
+    assert 'preexec_fn' not in kwargs, 'Use detached=True instead'
+
+    # Windows version of subprocess.Popen() really doens't like unicode. In
+    # practice we should use the current ANSI code page, but settle for utf-8
+    # across all OSes for consistency.
+    to_str = lambda i: i if isinstance(i, str) else i.encode('utf-8')
+    args = [to_str(i) for i in args]
+    if kwargs.get('cwd') is not None:
+      kwargs['cwd'] = to_str(kwargs['cwd'])
+    if kwargs.get('env'):
+      kwargs['env'] = {
+        to_str(k): to_str(v) for k, v in kwargs['env'].items()
+      }
+
+    self.start = time.time()
+    self.end = None
+    self.gid = None
+    self.detached = kwargs.pop('detached', False)
+    if self.detached:
+      if mswindows:
+        kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
+      else:
+        kwargs['preexec_fn'] = lambda: os.setpgid(0, 0)
+    with self.popen_lock:
+      super(Popen, self).__init__(args, **kwargs)
+    self.args = args
+    self.text = text
+    if self.detached and not mswindows:
+      try:
+        self.gid = os.getpgid(self.pid)
+      except OSError:
+        # sometimes the process can run+finish before we collect its pgid. fun.
+        pass
+
+  def duration(self):
+    """Duration of the child process.
+
+    It is greater or equal to the actual time the child process ran. It can be
+    significantly higher than the real value if neither .wait() nor .poll() was
+    used.
+    """
+    return (self.end or time.time()) - self.start
+
+  # pylint: disable=arguments-differ,redefined-builtin
+  def communicate(self, input=None, timeout=None):
+    """Implements python3's timeout support.
+
+    Unlike wait(), timeout=0 is considered the same as None.
+
+    Raises:
+    - TimeoutExpired when more than timeout seconds were spent waiting for the
+      process.
+    """
+    if not timeout:
+      return super(Popen, self).communicate(input=input)
+
+    assert isinstance(timeout, (int, float)), timeout
+
+    if self.stdin or self.stdout or self.stderr:
+      if self.stdout:
+        stdout = u'' if self.text else b''
+      else:
+        stdout = None
+      if self.stderr:
+        stderr = u'' if self.text else b''
+      else:
+        stderr = None
+
+      t = None
+      if input is not None:
+        assert self.stdin, (
+            'Can\'t use communicate(input) if not using '
+            'Popen(stdin=subprocess42.PIPE')
+        # TODO(maruel): Switch back to non-threading.
+        def write():
+          try:
+            self.stdin.write(input)
+          except IOError:
+            pass
+        t = threading.Thread(name='Popen.communicate', target=write)
+        t.daemon = True
+        t.start()
+
+      try:
+        if self.stdout or self.stderr:
+          start = time.time()
+          end = start + timeout
+          def remaining():
+            return max(end - time.time(), 0)
+          for pipe, data in self.yield_any(timeout=remaining):
+            if pipe is None:
+              raise TimeoutExpired(self.args, timeout, stdout, stderr)
+            assert pipe in ('stdout', 'stderr'), pipe
+            if pipe == 'stdout':
+              stdout += data
+            else:
+              stderr += data
+        else:
+          # Only stdin is piped.
+          self.wait(timeout=timeout)
+      finally:
+        if t:
+          try:
+            self.stdin.close()
+          except IOError:
+            pass
+          t.join()
+    else:
+      # No pipe. The user wanted to use wait().
+      self.wait(timeout=timeout)
+      return None, None
+
+    # Indirectly initialize self.end.
+    self.wait()
+    return stdout, stderr
+
+  def poll(self):
+    ret = super(Popen, self).poll()
+    if ret is not None and not self.end:
+      self.end = time.time()
+    return ret
+
+  def yield_any_line(self, **kwargs):
+    """Yields lines until the process terminates.
+
+    Like yield_any, but yields lines.
+    """
+    return split(self.yield_any(**kwargs))
+
+  def yield_any(self, maxsize=None, timeout=None):
+    """Yields output until the process terminates.
+
+    Unlike wait(), does not raise TimeoutExpired.
+
+    Yields:
+      (pipename, data) where pipename is either 'stdout', 'stderr' or None in
+      case of timeout or when the child process closed one of the pipe(s) and
+      all pending data on the pipe was read.
+
+    Arguments:
+    - maxsize: See recv_any(). Can be a callable function.
+    - timeout: If None, the call is blocking. If set, yields None, None if no
+          data is available within |timeout| seconds. It resets itself after
+          each yield. Can be a callable function.
+    """
+    assert self.stdout or self.stderr
+    if timeout is not None:
+      # timeout=0 effectively means that the pipe is continuously polled.
+      if isinstance(timeout, (int, float)):
+        assert timeout >= 0, timeout
+        old_timeout = timeout
+        timeout = lambda: old_timeout
+      else:
+        assert callable(timeout), timeout
+
+    if maxsize is not None and not callable(maxsize):
+      assert isinstance(maxsize, (int, float)), maxsize
+
+    last_yield = time.time()
+    while self.poll() is None:
+      to = timeout() if timeout else None
+      if to is not None:
+        to = max(to - (time.time() - last_yield), 0)
+      t, data = self.recv_any(
+          maxsize=maxsize() if callable(maxsize) else maxsize, timeout=to)
+      if data or to == 0:
+        yield t, data
+        last_yield = time.time()
+
+    # Read all remaining output in the pipes.
+    # There is 3 cases:
+    # - pipes get closed automatically by the calling process before it exits
+    # - pipes are closed automated by the OS
+    # - pipes are kept open due to grand-children processes outliving the
+    #   children process.
+    while True:
+      ms = maxsize
+      if callable(maxsize):
+        ms = maxsize()
+      # timeout=0 is mainly to handle the case where a grand-children process
+      # outlives the process started.
+      t, data = self.recv_any(maxsize=ms, timeout=0)
+      if not data:
+        break
+      yield t, data
+
+  def recv_any(self, maxsize=None, timeout=None):
+    """Reads from the first pipe available from stdout and stderr.
+
+    Unlike wait(), does not throw TimeoutExpired.
+
+    Arguments:
+    - maxsize: Maximum number of bytes to return. Defaults to MAX_SIZE.
+    - timeout: If None, it is blocking. If 0 or above, will return None if no
+          data is available within |timeout| seconds.
+
+    Returns:
+      tuple(pipename or None, str(data)). pipename is one of 'stdout' or
+      'stderr'.
+    """
+    # recv_multi_impl will early exit on a closed connection. Loop accordingly
+    # to simplify call sites.
+    while True:
+      pipes = [
+        x for x in ((self.stderr, 'stderr'), (self.stdout, 'stdout')) if x[0]
+      ]
+      # If both stdout and stderr have the exact file handle, they are
+      # effectively the same pipe. Deduplicate it since otherwise it confuses
+      # recv_multi_impl().
+      if len(pipes) == 2 and self.stderr.fileno() == self.stdout.fileno():
+        pipes.pop(0)
+
+      if not pipes:
+        return None, None
+      start = time.time()
+      conns, names = list(zip(*pipes))
+      index, data, closed = recv_multi_impl(conns, maxsize, timeout)
+      if index is None:
+        return index, data
+      if closed:
+        self._close(names[index])
+        if not data:
+          # Loop again. The other pipe may still be open.
+          if timeout:
+            timeout -= (time.time() - start)
+          continue
+
+      if self.text and data:
+        data = data.decode(
+            encoding=locale.getpreferredencoding(), errors='replace')
+        if self.universal_newlines:
+          data = self._translate_newlines(data)
+      return names[index], data
+
+  def recv_out(self, maxsize=None, timeout=None):
+    """Reads from stdout synchronously with timeout."""
+    return self._recv('stdout', maxsize, timeout)
+
+  def recv_err(self, maxsize=None, timeout=None):
+    """Reads from stderr synchronously with timeout."""
+    return self._recv('stderr', maxsize, timeout)
+
+  def terminate(self):
+    """Tries to do something saner on Windows that the stdlib.
+
+    Windows:
+      self.detached/CREATE_NEW_PROCESS_GROUP determines what can be used:
+      - If set, only SIGBREAK can be sent and it is sent to a single process.
+      - If not set, in theory only SIGINT can be used and *all processes* in
+         the processgroup receive it. In practice, we just kill the process.
+      See http://msdn.microsoft.com/library/windows/desktop/ms683155.aspx
+      The default on Windows is to call TerminateProcess() always, which is not
+      useful.
+
+    On Posix, always send SIGTERM.
+    """
+    try:
+      if mswindows and self.detached:
+        return self.send_signal(signal.CTRL_BREAK_EVENT)
+      super(Popen, self).terminate()
+    except OSError:
+      # The function will throw if the process terminated in-between. Swallow
+      # this.
+      pass
+
+  def kill(self):
+    """Kills the process and its children if possible.
+
+    Swallows exceptions and return True on success.
+    """
+    if self.gid:
+      try:
+        os.killpg(self.gid, signal.SIGKILL)
+      except OSError:
+        return False
+    else:
+      try:
+        super(Popen, self).kill()
+      except OSError:
+        return False
+    return True
+
+  def _close(self, which):
+    """Closes either stdout or stderr."""
+    getattr(self, which).close()
+    setattr(self, which, None)
+
+  def _recv(self, which, maxsize, timeout):
+    """Reads from one of stdout or stderr synchronously with timeout."""
+    conn = getattr(self, which)
+    if conn is None:
+      return None
+    _, data, closed = recv_multi_impl([conn], maxsize, timeout)
+    if closed:
+      self._close(which)
+
+    if self.text and data:
+      data = data.decode(
+          encoding=locale.getpreferredencoding(), errors='replace')
+      if self.universal_newlines:
+        data = self._translate_newlines(data)
+    return data
+
+
+@contextlib.contextmanager
+def set_signal_handler(signals, handler):
+  """Temporarilly override signals handler.
+
+  Useful when waiting for a child process to handle signals like SIGTERM, so the
+  signal can be propagated to the child process.
+  """
+  previous = {s: signal.signal(s, handler) for s in signals}
+  try:
+    yield
+  finally:
+    for sig, h in previous.items():
+      signal.signal(sig, h)
+
+
+def call(*args, **kwargs):
+  """Adds support for timeout."""
+  timeout = kwargs.pop('timeout', None)
+  return Popen(*args, **kwargs).wait(timeout)
+
+
+def check_call(*args, **kwargs):
+  """Adds support for timeout."""
+  retcode = call(*args, **kwargs)
+  if retcode:
+    raise subprocess.CalledProcessError(retcode, kwargs.get('args') or args[0])
+  return 0
+
+
+def check_output(*args, **kwargs):
+  """Adds support for timeout."""
+  timeout = kwargs.pop('timeout', None)
+  if 'stdout' in kwargs:
+    raise ValueError('stdout argument not allowed, it will be overridden.')
+  process = Popen(stdout=subprocess.PIPE, *args, **kwargs)
+  output, _ = process.communicate(timeout=timeout)
+  retcode = process.poll()
+  if retcode:
+    raise subprocess.CalledProcessError(retcode, kwargs.get('args') or args[0], output)
+  return output
+
+
+def call_with_timeout(args, timeout, **kwargs):
+  """Runs an executable; kill it in case of timeout."""
+  proc = Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+               stderr=subprocess.PIPE, **kwargs)
+  try:
+    out, err = proc.communicate(timeout=timeout)
+  except TimeoutExpired as e:
+    out = e.output
+    err = e.stderr
+    proc.kill()
+    proc.wait()
+  return out, err, proc.returncode, proc.duration()
+
+
+def inhibit_os_error_reporting():
+  """Inhibits error reporting UI and core files.
+
+  This function should be called as early as possible in the process lifetime.
+  """
+  global _OS_ERROR_REPORTING_INHIBITED
+  if not _OS_ERROR_REPORTING_INHIBITED:
+    _OS_ERROR_REPORTING_INHIBITED = True
+    if sys.platform == 'win32':
+      # Windows has a bad habit of opening a dialog when a console program
+      # crashes, rather than just letting it crash. Therefore, when a program
+      # crashes on Windows, we don't find out until the build step times out.
+      # This code prevents the dialog from appearing, so that we find out
+      # immediately and don't waste time waiting for a user to close the dialog.
+      # https://msdn.microsoft.com/en-us/library/windows/desktop/ms680621.aspx
+      SEM_FAILCRITICALERRORS = 1
+      SEM_NOGPFAULTERRORBOX = 2
+      SEM_NOALIGNMENTFAULTEXCEPT = 0x8000
+      ctypes.windll.kernel32.SetErrorMode(
+          SEM_FAILCRITICALERRORS|SEM_NOGPFAULTERRORBOX|
+            SEM_NOALIGNMENTFAULTEXCEPT)
+  # TODO(maruel): Other OSes.
+  # - OSX, need to figure out a way to make the following process tree local:
+  #     defaults write com.apple.CrashReporter UseUNC 1
+  #     defaults write com.apple.CrashReporter DialogType none
+  # - Ubuntu, disable apport if needed.
+
+
+def split(data, sep='\n'):
+  """Splits pipe data by |sep|. Does some buffering.
+
+  For example, [('stdout', 'a\nb'), ('stdout', '\n'), ('stderr', 'c\n')] ->
+  [('stdout', 'a'), ('stdout', 'b'), ('stderr', 'c')].
+
+  Args:
+    data: iterable of tuples (pipe_name, bytes).
+
+  Returns:
+    An iterator of tuples (pipe_name, bytes) where bytes is the input data
+    but split by sep into separate tuples.
+  """
+  # A dict {pipe_name -> list of pending chunks without separators}
+  pending_chunks = collections.defaultdict(list)
+  for pipe_name, chunk in data:
+    if chunk is None:
+      # Happens if a pipe is closed.
+      continue
+
+    pending = pending_chunks[pipe_name]
+    start = 0  # offset in chunk to start |sep| search from
+    while start < len(chunk):
+      j = chunk.find(sep, start)
+      if j == -1:
+        pending_chunks[pipe_name].append(chunk[start:])
+        break
+
+      to_emit = chunk[start:j]
+      start = j + 1
+      if pending:
+        # prepend and forget
+        to_emit = ''.join(pending) + to_emit
+        pending = []
+        pending_chunks[pipe_name] = pending
+      yield pipe_name, to_emit
+
+  # Emit remaining chunks that don't end with separators as is.
+  for pipe_name, chunks in sorted(pending_chunks.items()):
+    if chunks:
+      yield pipe_name, ''.join(chunks)
diff --git a/doc/epydoc.cfg b/doc/epydoc.cfg
new file mode 100644
index 0000000..df3a6c5
--- /dev/null
+++ b/doc/epydoc.cfg
@@ -0,0 +1,16 @@
+[epydoc]
+
+output: html
+target: /var/www/eureka/git_doc/
+
+graph: all
+# TODO(gogan): figure out how to generate a pstat output for the full lib:
+#pstat: /tmp/git.testing.pstat
+#dotpath: /usr/bin/dot
+
+frames: no
+url: https://eureka-internal.googlesource.com/test/
+
+simple-term: yes
+
+name: Eureka Testing Library
diff --git a/doc/epydoc.css b/doc/epydoc.css
new file mode 100644
index 0000000..3ed1be2
--- /dev/null
+++ b/doc/epydoc.css
@@ -0,0 +1,692 @@
+/* Epydoc CSS Stylesheet
+*
+*  This is a massivley hacked stylesheet for epydoc, the very nice Python API
+*  Documentation generator. The general look and feel is based on sphinx.
+*
+*  Feel free to tweak this some more to make the theme even more pleasing 
+*  if you have the time to spare for such things. The typography in particular
+*  could do with sorting out. Unless you like writing masocistic CSS however,
+*  I'd recommend looking to refactor the markup generated by epydoc.
+*
+*/
+
+/* Yahoo! Reset Styles
+Copyright (c) 2008, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.net/yui/license.txt
+version: 2.5.1
+*/
+html {
+    color: #000;
+    background: #FFF;
+}
+body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td {
+    margin: 0;
+    padding: 0;
+}
+table {
+    border-collapse: collapse;
+    border-spacing: 0;
+}
+fieldset,img {
+    border: 0;
+}
+address,caption,cite,code,dfn,em,strong,th,var {
+    font-style: normal;
+    font-weight: normal;
+}
+li {
+    list-style: none;
+}
+caption,th {
+    text-align: left;
+}
+h1,h2,h3,h4,h5,h6 {
+    font-size: 100%;
+    font-weight: normal;
+}
+q:before,q:after {
+    content: '';
+}
+abbr,acronym {
+    border: 0;
+    font-variant: normal;
+}
+sup {
+    vertical-align: text-top;
+}
+sub {
+    vertical-align: text-bottom;
+}
+input,textarea,select {
+    font-family: inherit;
+    font-size: inherit;
+    font-weight: inherit;
+}
+input,textarea,select {*font-size:100%;
+}
+legend {
+    color: #000;
+}
+body {
+    font: 13px/1.231 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small;
+}
+table {
+    font-size: inherit;
+    font: 100%;
+}
+pre,code,kbd,samp,tt {
+    font-family: monospace;*font-size:108%;
+    line-height: 100%;
+}
+
+
+
+/* Default Colors & Styles
+* - Set the default foreground & background color with 'body'; and
+* link colors with 'a:link' and 'a:visited'.
+* - Use bold for decision list terms.
+* - The heading styles defined here are used for headings *within*
+* docstring descriptions. All headings used by epydoc itself use
+* either class='epydoc' or class='toc' (CSS styles for both
+* defined below).
+*/
+body {
+    background: #ffffff;
+    color: #000000;
+}
+p {
+    margin-top: 0.5em;
+    margin-bottom: 0.5em;
+}
+a {
+    color: #355f7c;
+    text-decoration: none;
+}
+
+a:hover {
+    text-decoration: underline;
+}
+
+dt {
+    font-weight: bold;
+}
+h1, h2 {
+    color: #20435C;
+    background: #F2F2F2;
+    border-bottom: 1px solid #CCC;
+    padding: 10px;
+}
+center {
+    display: none;
+}
+code {
+    font-size: 100%;
+}
+/* N.B.: class, not pseudoclass */
+a.link {
+    font-family: monospace;
+}
+
+/* Page Header & Footer
+* - The standard page header consists of a navigation bar (with
+* pointers to standard pages such as 'home' and 'trees'); a
+* breadcrumbs list, which can be used to navigate to containing
+* classes or modules; options links, to show/hide private
+* variables and to show/hide frames; and a page title (using
+* <h1>). The page title may be followed by a link to the
+* corresponding source code (using 'span.codelink').
+* - The footer consists of a navigation bar, a timestamp, and a
+* pointer to epydoc's homepage.
+*/
+h1.epydoc {
+    margin: -14px 0 0;
+    font-size: +140%;
+    color: #20435C;
+}
+h2.epydoc {
+    font-size: +130%;
+}
+h3.epydoc {
+    font-size: +115%;
+    margin-top: 0.2em;
+    margin-left: 25px;
+}
+td h3.epydoc {
+    font-size: +115%;
+    font-weight: bold;
+    margin-bottom: 0;
+}
+table.navbar {
+    background: #133F52;
+    color: #355f7c;
+    border: 0;
+}
+table.navbar table {
+    color: #355f7c;
+}
+th.navbar-select {
+    background: #1C4E63;
+    color: #FFF;
+}
+table.navbar table th {
+    padding-left: 15px;
+}
+table.navbar a {
+    text-decoration: none;
+    color: #FFF;
+    padding: 0 10px;
+    display: block;
+}
+table td.footer {
+    padding: 10px;
+    color: #666;
+    font-size: 90%;
+}
+/* Table Headers
+* - Each summary table and details section begins with a 'header'
+* row. This row contains a section title (marked by
+* 'span.table-header') as well as a show/hide private link
+* (marked by 'span.options', defined above).
+* - Summary tables that contain user-defined groups mark those
+* groups using 'group header' rows.
+*/
+td.table-header {
+    background: #F2F2F2;
+    color: #20435C;
+    border: 1px solid #CCC;
+    padding: 5px 10px;
+}
+td.table-header table a:visited {
+    color: #204080;
+}
+span.table-header {
+    font-size: 120%;
+}
+th.group-header {
+    background: #c0e0f8;
+    color: #000000;
+    text-align: left;
+    font-size: 115%;
+    border: 1px solid #608090;
+}
+
+/* Summary Tables (functions, variables, etc)
+* - Each object is described by a single row of the table with
+* two cells. The left cell gives the object's type, and is
+* marked with 'code.summary-type'. The right cell gives the
+* object's name and a summary description.
+* - CSS styles for the table's header and group headers are
+* defined above, under 'Table Headers'
+*/
+table.summary {
+    border-collapse: collapse;
+    background: #FFF;
+    color: #000000;
+    border: 1px solid #CCC;
+    margin: 0 0 20px;
+}
+td.summary {
+    border: 1px solid #CCC;
+    padding: 5px 10px;
+}
+code.summary-type {
+    font-size: 85%;
+}
+
+/* Details Tables (functions, variables, etc)
+* - Each object is described in its own div.
+* - A single-row summary table w/ table-header is used as
+* a header for each details section (CSS style for table-header
+* is defined above, under 'Table Headers').
+*/
+table.details {
+    border-collapse: collapse;
+    background: #FFF;
+    color: #000000;
+    border: 0;
+    margin: .2em 0 0 0;
+}
+table.details table {
+    color: #000000;
+}
+
+/* Fields */
+dl.fields {
+    margin-left: 2em;
+    margin-top: 1em;
+    margin-bottom: 1em;
+}
+dl.fields dd ul {
+    margin-left: 0em;
+    padding-left: 0em;
+}
+dl.fields dd ul li ul {
+    margin-left: 2em;
+    padding-left: 0em;
+}
+div.fields {
+    margin-left: 2em;
+}
+div.fields p {
+    margin-bottom: 0.5em;
+}
+
+/* Index tables (identifier index, term index, etc)
+* - link-index is used for indices containing lists of links
+* (namely, the identifier index & term index).
+* - index-where is used in link indices for the text indicating
+* the container/source for each link.
+* - metadata-index is used for indices containing metadata
+* extracted from fields (namely, the bug index & todo index).
+*/
+table.link-index {
+    border-collapse: collapse;
+    background: #F2F2F2;
+    color: #000000;
+    border: 1px solid #CCC;
+}
+td.link-index {
+    border-width: 0px;
+    padding: 3px;
+}
+span.index-where {
+    font-size: 70%;
+}
+table.metadata-index {
+    border-collapse: collapse;
+    background: #e8f0f8;
+    color: #000000;
+    border: 1px solid #608090;
+    margin: .2em 0 0 0;
+}
+td.metadata-index {
+    border-width: 1px;
+    border-style: solid;
+}
+table.link-index h2.epydoc {
+    border-width: 1px 0;
+}
+
+/* Function signatures
+* - sig* is used for the signature in the details section.
+* - .summary-sig* is used for the signature in the summary
+* table, and when listing property accessor functions.
+* */
+.sig-name {
+    color: #006080;
+}
+.sig-arg {
+    color: #008060;
+}
+.sig-default {
+    color: #602000;
+}
+.summary-sig {
+    font-family: monospace;
+}
+.summary-sig-name {
+    color: #006080;
+    font-weight: bold;
+}
+table.summary a.summary-sig-name:link {
+    color: #006080;
+    font-weight: bold;
+}
+table.summary a.summary-sig-name:visited {
+    color: #006080;
+    font-weight: bold;
+}
+.summary-sig-arg {
+    color: #006040;
+}
+.summary-sig-default {
+    color: #501800;
+}
+
+/* Subclass list
+*/
+ul.subclass-list {
+    display: inline;
+}
+ul.subclass-list li {
+    display: inline;
+}
+
+/* To render variables, classes etc. like functions */
+table.summary .summary-name {
+    color: #006080;
+    font-weight: bold;
+    font-family: monospace;
+}
+table.summary
+a.summary-name:link {
+    color: #006080;
+    font-weight: bold;
+    font-family: monospace;
+}
+table.summary
+a.summary-name:visited {
+    color: #006080;
+    font-weight: bold;
+    font-family: monospace;
+}
+
+/* Variable values
+* - In the 'variable details' sections, each varaible's value is
+* listed in a 'pre.variable' box. The width of this box is
+* restricted to 80 chars; if the value's repr is longer than
+* this it will be wrapped, using a backslash marked with
+* class 'variable-linewrap'. If the value's repr is longer
+* than 3 lines, the rest will be ellided; and an ellipsis
+* marker ('...' marked with 'variable-ellipsis') will be used.
+* - If the value is a string, its quote marks will be marked
+* with 'variable-quote'.
+* - If the variable is a regexp, it is syntax-highlighted using
+* the re* CSS classes.
+*/
+pre.variable {
+    padding: .5em;
+    margin: 0;
+    background: #F2F2F2;
+    color: #000000;
+    border: 1px solid #CCC;
+}
+.variable-linewrap {
+    color: #604000;
+    font-weight: bold;
+}
+.variable-ellipsis {
+    color: #604000;
+    font-weight: bold;
+}
+.variable-quote {
+    color: #604000;
+    font-weight: bold;
+}
+.variable-group {
+    color: #008000;
+    font-weight: bold;
+}
+.variable-op {
+    color: #604000;
+    font-weight: bold;
+}
+.variable-string {
+    color: #006030;
+}
+.variable-unknown {
+    color: #a00000;
+    font-weight: bold;
+}
+.re {
+    color: #000000;
+}
+.re-char {
+    color: #006030;
+}
+.re-op {
+    color: #600000;
+}
+.re-group {
+    color: #003060;
+}
+.re-ref {
+    color: #404040;
+}
+
+/* Base tree
+* - Used by class pages to display the base class hierarchy.
+*/
+pre.base-tree {
+    font-size: 80%;
+    margin: 0;
+}
+
+
+/* Syntax Highlighting for Source Code
+* - doctest examples are displayed in a 'pre.py-doctest' block.
+* If the example is in a details table entry, then it will use
+* the colors specified by the 'table pre.py-doctest' line.
+* - Source code listings are displayed in a 'pre.py-src' block.
+* Each line is marked with 'span.py-line' (used to draw a line
+* down the left margin, separating the code from the line
+* numbers). Line numbers are displayed with 'span.py-lineno'.
+* The expand/collapse block toggle button is displayed with
+* 'a.py-toggle' (Note: the CSS style for 'a.py-toggle' should not
+* modify the font size of the text.)
+* - If a source code page is opened with an anchor, then the
+* corresponding code block will be highlighted. The code
+* block's header is highlighted with 'py-highlight-hdr'; and
+* the code block's body is highlighted with 'py-highlight'.
+* - The remaining py-* classes are used to perform syntax
+* highlighting (py-string for string literals, py-name for names,
+* etc.)
+*/
+pre.py-doctest {
+    padding: .5em;
+    margin: 1em;
+    background: #F2F2F2;
+    color: #000000;
+    border: 1px solid #CCC;
+}
+table pre.py-doctest {
+    background: #F2F2F2;
+    color: #000000;
+}
+pre.py-src {
+    border: 1px solid #CCC;
+    background: #FFF;
+    color: #000000;
+    margin: 20px 0;
+}
+.py-line {
+    border-left: 1px solid #CCC;
+    margin-left: .2em;
+    padding-left: .4em;
+}
+.py-lineno {
+    font-style: italic;
+    font-size: 90%;
+    padding-left: .5em;
+}
+a.py-toggle {
+    text-decoration: none;
+}
+div.py-highlight-hdr {
+    border-top: 1px solid #CCC;
+    border-bottom: 1px solid #CCC;
+    background: #F2F2F2;
+}
+div.py-highlight {
+    border-bottom: 1px solid #CCC;
+    background: #F2F2F2;
+}
+.py-prompt {
+    color: #005050;
+    font-weight: bold;
+}
+.py-more {
+    color: #005050;
+    font-weight: bold;
+}
+.py-string {
+    color: #006030;
+}
+.py-comment {
+    color: #003060;
+}
+.py-keyword {
+    color: #600000;
+}
+.py-output {
+    color: #404040;
+}
+.py-name {
+    color: #000050;
+}
+.py-name:link {
+    color: #000050 !important;
+}
+.py-name:visited {
+    color: #000050 !important;
+}
+.py-number {
+    color: #005000;
+}
+.py-defname {
+    color: #000060;
+    font-weight: bold;
+}
+.py-def-name {
+    color: #000060;
+    font-weight: bold;
+}
+.py-base-class {
+    color: #000060;
+}
+.py-param {
+    color: #000060;
+}
+.py-docstring {
+    color: #006030;
+}
+.py-decorator {
+    color: #804020;
+}
+
+/* Graphs & Diagrams
+* - These CSS styles are used for graphs & diagrams generated using
+* Graphviz dot. 'img.graph-without-title' is used for bare
+* diagrams (to remove the border created by making the image
+* clickable).
+*/
+img.graph-without-title {
+    border: none;
+}
+img.graph-with-title {
+    border: 1px solid #000000;
+}
+span.graph-title {
+    font-weight: bold;
+}
+
+/* General-purpose classes
+* - 'p.indent-wrapped-lines' defines a paragraph whose first line
+* is not indented, but whose subsequent lines are.
+* - The 'nomargin-top' class is used to remove the top margin (e.g.
+* from lists). The 'nomargin' class is used to remove both the
+* top and bottom margin (but not the left or right margin --
+* for lists, that would cause the bullets to disappear.)
+*/
+p.indent-wrapped-lines {
+    padding: 0 0 0 7em;
+    text-indent: -7em;
+    margin: 0;
+}
+.nomargin-top {
+    margin: 20px;
+}
+.nomargin {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+/* HTML Log */
+div.log-block {
+    padding: 0;
+    margin: .5em 0 .5em 0;
+    background: #e8f0f8;
+    color: #000000;
+    border: 1px solid #000000;
+}
+div.log-error {
+    padding: .1em .3em .1em .3em;
+    margin: 4px;
+    background: #ffb0b0;
+    color: #000000;
+    border: 1px solid #000000;
+}
+div.log-warning {
+    padding: .1em .3em .1em .3em;
+    margin: 4px;
+    background: #ffffb0;
+    color: #000000;
+    border: 1px solid #000000;
+}
+div.log-info {
+    padding: .1em .3em .1em .3em;
+    margin: 4px;
+    background: #b0ffb0;
+    color: #000000;
+    border: 1px solid #000000;
+}
+h2.log-hdr {
+    background: #70b0ff;
+    color: #000000;
+    margin: 0;
+    padding: 0em 0.5em 0em 0.5em;
+    border-bottom: 1px solid #000000;
+    font-size: 110%;
+}
+p.log {
+    font-weight: bold;
+    margin: .5em 0 .5em 0;
+}
+tr.opt-changed {
+    color: #000000;
+    font-weight: bold;
+}
+tr.opt-default {
+    color: #606060;
+}
+pre.log {
+    margin: 0;
+    padding: 0;
+    padding-left: 1em;
+}
+
+/* Hacks
+*  This section is reseerved for the really horrible bits. In reality the
+*  markup generated by epydoc wants a good kickings but a CSS patch is 
+*  better than nothing. Some of this does remove useful links or output
+*  because finding somewhere to put them on the page wasn't easy. If your
+*  favourite link has disappeared this is probably the reason.
+*/
+
+ul ul {
+    margin-left: 1em;
+}
+ul ul ul {
+    margin-left: 2em;
+}
+ul ul ul ul {
+    margin-left: 3em;
+}
+a[target=mainFrame], a[target=moduleFrame] {
+    margin: 0 10px;
+}
+h2.toc {
+    margin-bottom: 10px;
+}
+h1.toc {
+    display: none;
+}
+.options {
+    display: none;
+}
+hr, br {
+    display: none;
+}
+a[href="toc-everything.html"] {
+    display: none;
+}
+.breadcrumbs {
+    visibility: hidden;
+}
+.details p {
+    margin-left: 25px;
+}
+body > p {
+    margin: 20px;
+}
diff --git a/essential_pkg.yaml b/essential_pkg.yaml
new file mode 100644
index 0000000..aa3be8f
--- /dev/null
+++ b/essential_pkg.yaml
@@ -0,0 +1,11 @@
+package: home_internal/continuous-tests
+description: continuous tests config files required for swarming host.
+root: .
+data:
+  - dir: lab_system/master/config/device_type
+  - dir: lab_system/master/config/site
+  - dir: lab_system/master/config/tests
+  - dir: catatester/configs/prod
+  - dir: catatester/genfiles
+  - file: __init__.py
+  - file: catatester/__init__.py
diff --git a/lab_system/master/config/android/device_name.json b/lab_system/master/config/android/device_name.json
new file mode 100644
index 0000000..8d15d50
--- /dev/null
+++ b/lab_system/master/config/android/device_name.json
@@ -0,0 +1,43 @@
+{
+    "D6503": "SonyZ2",
+    "D6616": "SonyZ3",
+    "E5803": "SonyZ5C",
+    "E6653": "SonyZ5",
+    "E6853": "SonyZ5P",
+    "Yaris5TM0": "Alcatel7040N",
+    "angler": "Nexus6P",
+    "bullhead": "Nexus5X",
+    "d2spr": "S3",
+    "degaswifiue": "GalaxyTab4",
+    "dragon": "PixelC",
+    "e2nam": "OptimusF60",
+    "e7lte": "GPad7",
+    "falcon_umts": "MotoG",
+    "flo": "Nexus7",
+    "flounder": "Nexus9",
+    "heroqlteatt": "S7",
+    "heroqlteusc": "S7",
+    "hero2qlteatt": "S7Edge",
+    "g2": "G2",
+    "g3": "G3",
+    "hammerhead": "Nexus5",
+    "herolte": "S7",
+    "hero2lte": "S7Edge",
+    "hlteatt": "Note3",
+    "htc_m8": "M8",
+    "ja3g": "S4",
+    "jflteatt": "S4",
+    "klteatt": "S5",
+    "lt02ltespr": "GalaxyTab3",
+    "m7": "M7",
+    "noblelteatt": "Note5",
+    "noblelteskt": "Note5",
+    "shamu": "Nexus6",
+    "trlteatt": "Note4",
+    "w5": "OptimusL70",
+    "w7": "OptimusL90",
+    "zenlte": "S6EdgePlus",
+    "zenlteatt": "S6EdgePlus",
+    "zerolteatt": "S6Edge",
+    "zeroflteatt": "S6"
+}
diff --git a/lab_system/master/config/branch/1_22.cfg b/lab_system/master/config/branch/1_22.cfg
new file mode 100644
index 0000000..2de4150
--- /dev/null
+++ b/lab_system/master/config/branch/1_22.cfg
@@ -0,0 +1,14 @@
+# Config file for 1.22 branch.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.22
+
+SCHEDULER=45 * * * *
+
+USE_GN=1
diff --git a/lab_system/master/config/branch/1_26a.cfg b/lab_system/master/config/branch/1_26a.cfg
new file mode 100644
index 0000000..e5d6d5b
--- /dev/null
+++ b/lab_system/master/config/branch/1_26a.cfg
@@ -0,0 +1,15 @@
+# Config file for release branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use for the run. Change it to the latest release branch.
+BRANCH_NAME=1.26a
+
+# How often the tests will run
+#SCHEDULER=31 0,8,16 * * *
+
+USE_GN=1
diff --git a/lab_system/master/config/branch/1_27_factory.cfg b/lab_system/master/config/branch/1_27_factory.cfg
new file mode 100644
index 0000000..d0040e1
--- /dev/null
+++ b/lab_system/master/config/branch/1_27_factory.cfg
@@ -0,0 +1,15 @@
+# Config file for branch for 1.27 Factory build
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use for the run. Change it to the next release branch.
+BRANCH_NAME=1.27f
+
+# How often the tests will run
+SCHEDULER=15 */2 * * *
+
+USE_GN=1
diff --git a/lab_system/master/config/branch/1_28_factory.cfg b/lab_system/master/config/branch/1_28_factory.cfg
new file mode 100644
index 0000000..246ff89
--- /dev/null
+++ b/lab_system/master/config/branch/1_28_factory.cfg
@@ -0,0 +1,15 @@
+# Config file for branch for 1.28 Factory build
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use for the run. Change it to the next release branch.
+BRANCH_NAME=1.28f
+
+# How often the tests will run
+SCHEDULER=15 */2 * * *
+
+USE_GN=1
diff --git a/lab_system/master/config/branch/1_28a.cfg b/lab_system/master/config/branch/1_28a.cfg
new file mode 100644
index 0000000..bcb410b
--- /dev/null
+++ b/lab_system/master/config/branch/1_28a.cfg
@@ -0,0 +1,15 @@
+# Config file for branch for 1.28a build
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use for the run. Change it to the next release branch.
+BRANCH_NAME=1.28a
+
+# How often the tests will run
+SCHEDULER=15 */2 * * *
+
+USE_GN=1
diff --git a/lab_system/master/config/branch/androidthings_next_release.cfg b/lab_system/master/config/branch/androidthings_next_release.cfg
new file mode 100644
index 0000000..0631bee
--- /dev/null
+++ b/lab_system/master/config/branch/androidthings_next_release.cfg
@@ -0,0 +1,12 @@
+# Config file for AndroidThings next release branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use for the run. Change it to the latest release branch.
+BRANCH_NAME=1.26at
+
+USE_GN=1
diff --git a/lab_system/master/config/branch/androidthings_release.cfg b/lab_system/master/config/branch/androidthings_release.cfg
new file mode 100644
index 0000000..bfc1ca4
--- /dev/null
+++ b/lab_system/master/config/branch/androidthings_release.cfg
@@ -0,0 +1,12 @@
+# Config file for AndroidThings release branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use for the run. Change it to the latest release branch.
+BRANCH_NAME=1.25at
+
+USE_GN=1
diff --git a/lab_system/master/config/branch/cast_assistant_reference_master.cfg b/lab_system/master/config/branch/cast_assistant_reference_master.cfg
new file mode 100644
index 0000000..7992dfe
--- /dev/null
+++ b/lab_system/master/config/branch/cast_assistant_reference_master.cfg
@@ -0,0 +1,10 @@
+# Config file for cast assistant reference branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=master
diff --git a/lab_system/master/config/branch/cast_assistant_reference_release.cfg b/lab_system/master/config/branch/cast_assistant_reference_release.cfg
new file mode 100644
index 0000000..88fb62b
--- /dev/null
+++ b/lab_system/master/config/branch/cast_assistant_reference_release.cfg
@@ -0,0 +1,10 @@
+# Config file for cast assistant reference branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.26
diff --git a/lab_system/master/config/branch/cast_audio_mediatek_master.cfg b/lab_system/master/config/branch/cast_audio_mediatek_master.cfg
new file mode 100644
index 0000000..d09aaf2
--- /dev/null
+++ b/lab_system/master/config/branch/cast_audio_mediatek_master.cfg
@@ -0,0 +1,10 @@
+# Config file for cast audio branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=master
diff --git a/lab_system/master/config/branch/cast_audio_mediatek_next_release.cfg b/lab_system/master/config/branch/cast_audio_mediatek_next_release.cfg
new file mode 100644
index 0000000..521826c
--- /dev/null
+++ b/lab_system/master/config/branch/cast_audio_mediatek_next_release.cfg
@@ -0,0 +1,10 @@
+# Config file for cast audio branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.27
diff --git a/lab_system/master/config/branch/cast_audio_mediatek_release.cfg b/lab_system/master/config/branch/cast_audio_mediatek_release.cfg
new file mode 100644
index 0000000..521826c
--- /dev/null
+++ b/lab_system/master/config/branch/cast_audio_mediatek_release.cfg
@@ -0,0 +1,10 @@
+# Config file for cast audio branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.27
diff --git a/lab_system/master/config/branch/cast_audio_reference_master.cfg b/lab_system/master/config/branch/cast_audio_reference_master.cfg
new file mode 100644
index 0000000..6983d58
--- /dev/null
+++ b/lab_system/master/config/branch/cast_audio_reference_master.cfg
@@ -0,0 +1,10 @@
+# Config file for cast audio reference branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=master
diff --git a/lab_system/master/config/branch/cast_audio_reference_release.cfg b/lab_system/master/config/branch/cast_audio_reference_release.cfg
new file mode 100644
index 0000000..7ac81e9
--- /dev/null
+++ b/lab_system/master/config/branch/cast_audio_reference_release.cfg
@@ -0,0 +1,10 @@
+# Config file for cast audio reference branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.26
diff --git a/lab_system/master/config/branch/cast_tv_dragonfly_master.cfg b/lab_system/master/config/branch/cast_tv_dragonfly_master.cfg
new file mode 100644
index 0000000..1e55e22
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_dragonfly_master.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv dragonfly branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=master
diff --git a/lab_system/master/config/branch/cast_tv_dragonfly_next_release.cfg b/lab_system/master/config/branch/cast_tv_dragonfly_next_release.cfg
new file mode 100644
index 0000000..44a09fd
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_dragonfly_next_release.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv dragonfly branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.27
diff --git a/lab_system/master/config/branch/cast_tv_dragonfly_release.cfg b/lab_system/master/config/branch/cast_tv_dragonfly_release.cfg
new file mode 100644
index 0000000..dfef1bb
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_dragonfly_release.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv dragonfly branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.26
diff --git a/lab_system/master/config/branch/cast_tv_firefly_master.cfg b/lab_system/master/config/branch/cast_tv_firefly_master.cfg
new file mode 100644
index 0000000..ba0646c
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_firefly_master.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv firefly branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=master
diff --git a/lab_system/master/config/branch/cast_tv_marvell_master.cfg b/lab_system/master/config/branch/cast_tv_marvell_master.cfg
new file mode 100644
index 0000000..94cac1b
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_marvell_master.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv marvell branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=master
diff --git a/lab_system/master/config/branch/cast_tv_marvell_next_release.cfg b/lab_system/master/config/branch/cast_tv_marvell_next_release.cfg
new file mode 100644
index 0000000..6222f37
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_marvell_next_release.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv marvell branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.27
diff --git a/lab_system/master/config/branch/cast_tv_marvell_release.cfg b/lab_system/master/config/branch/cast_tv_marvell_release.cfg
new file mode 100644
index 0000000..6fdc7e3
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_marvell_release.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv marvell branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.26
diff --git a/lab_system/master/config/branch/cast_tv_mediatek_master.cfg b/lab_system/master/config/branch/cast_tv_mediatek_master.cfg
new file mode 100644
index 0000000..ba1b613
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_mediatek_master.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv Mars_US branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=master
diff --git a/lab_system/master/config/branch/cast_tv_mediatek_next_release.cfg b/lab_system/master/config/branch/cast_tv_mediatek_next_release.cfg
new file mode 100644
index 0000000..c153155
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_mediatek_next_release.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv Mars_US branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.27
diff --git a/lab_system/master/config/branch/cast_tv_mediatek_release.cfg b/lab_system/master/config/branch/cast_tv_mediatek_release.cfg
new file mode 100644
index 0000000..c4cd4d1
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_mediatek_release.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv Mars_US branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.26
diff --git a/lab_system/master/config/branch/cast_tv_mstar_master.cfg b/lab_system/master/config/branch/cast_tv_mstar_master.cfg
new file mode 100644
index 0000000..dcd749e
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_mstar_master.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv bubbletea branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=master
diff --git a/lab_system/master/config/branch/cast_tv_mstar_next_release.cfg b/lab_system/master/config/branch/cast_tv_mstar_next_release.cfg
new file mode 100644
index 0000000..aaa5c6a
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_mstar_next_release.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv bubbletea branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.27
diff --git a/lab_system/master/config/branch/cast_tv_mstar_release.cfg b/lab_system/master/config/branch/cast_tv_mstar_release.cfg
new file mode 100644
index 0000000..e982a38
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_mstar_release.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv bubbletea branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.26
diff --git a/lab_system/master/config/branch/cast_tv_novatek_master.cfg b/lab_system/master/config/branch/cast_tv_novatek_master.cfg
new file mode 100644
index 0000000..3d97fa7
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_novatek_master.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv master branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=master
diff --git a/lab_system/master/config/branch/cast_tv_novatek_next_release.cfg b/lab_system/master/config/branch/cast_tv_novatek_next_release.cfg
new file mode 100644
index 0000000..a1e81b5
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_novatek_next_release.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv next release branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.27
diff --git a/lab_system/master/config/branch/cast_tv_novatek_release.cfg b/lab_system/master/config/branch/cast_tv_novatek_release.cfg
new file mode 100644
index 0000000..0776912
--- /dev/null
+++ b/lab_system/master/config/branch/cast_tv_novatek_release.cfg
@@ -0,0 +1,10 @@
+# Config file for cast tv release branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=1.26
diff --git a/lab_system/master/config/branch/dogfood.cfg b/lab_system/master/config/branch/dogfood.cfg
new file mode 100644
index 0000000..a3cab64
--- /dev/null
+++ b/lab_system/master/config/branch/dogfood.cfg
@@ -0,0 +1,18 @@
+# Config file for dogfood branches
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in the run. Change it to the latest release branch.
+BRANCH_NAME=dogfood-release
+
+# Mirroring test chrome build
+CHROME_BUILD=33.0.1750.112
+# Mirroring test extension version
+EXTENSION_VERSION=2.2014.10215.10042
+
+# How often tests on the branch will run
+SCHEDULER=10 12 * * 0
diff --git a/lab_system/master/config/branch/klp-modular-dev.cfg b/lab_system/master/config/branch/klp-modular-dev.cfg
new file mode 100644
index 0000000..37a4f1e
--- /dev/null
+++ b/lab_system/master/config/branch/klp-modular-dev.cfg
@@ -0,0 +1,10 @@
+# Config file for molly branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=master
diff --git a/lab_system/master/config/branch/master.cfg b/lab_system/master/config/branch/master.cfg
new file mode 100644
index 0000000..834434d
--- /dev/null
+++ b/lab_system/master/config/branch/master.cfg
@@ -0,0 +1,14 @@
+# Config file for master branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in run
+BRANCH_NAME=master
+
+SCHEDULER=31 * * * *
+
+USE_GN=1
diff --git a/lab_system/master/config/branch/mirroring_earth_on_demand.cfg b/lab_system/master/config/branch/mirroring_earth_on_demand.cfg
new file mode 100644
index 0000000..d72c544
--- /dev/null
+++ b/lab_system/master/config/branch/mirroring_earth_on_demand.cfg
@@ -0,0 +1,14 @@
+# Config file for Whitelist on_demand branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name and build number match the current production release.
+BRANCH_NAME=1.30
+BUILD=113131
+
+# on demand branch will only run when forced.
+SCHEDULER=None
diff --git a/lab_system/master/config/branch/mirroring_eureka_on_demand.cfg b/lab_system/master/config/branch/mirroring_eureka_on_demand.cfg
new file mode 100644
index 0000000..d72c544
--- /dev/null
+++ b/lab_system/master/config/branch/mirroring_eureka_on_demand.cfg
@@ -0,0 +1,14 @@
+# Config file for Whitelist on_demand branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name and build number match the current production release.
+BRANCH_NAME=1.30
+BUILD=113131
+
+# on demand branch will only run when forced.
+SCHEDULER=None
diff --git a/lab_system/master/config/branch/mirroring_lexx_on_demand.cfg b/lab_system/master/config/branch/mirroring_lexx_on_demand.cfg
new file mode 100644
index 0000000..d72c544
--- /dev/null
+++ b/lab_system/master/config/branch/mirroring_lexx_on_demand.cfg
@@ -0,0 +1,14 @@
+# Config file for Whitelist on_demand branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name and build number match the current production release.
+BRANCH_NAME=1.30
+BUILD=113131
+
+# on demand branch will only run when forced.
+SCHEDULER=None
diff --git a/lab_system/master/config/branch/mirroring_luther_on_demand.cfg b/lab_system/master/config/branch/mirroring_luther_on_demand.cfg
new file mode 100644
index 0000000..d72c544
--- /dev/null
+++ b/lab_system/master/config/branch/mirroring_luther_on_demand.cfg
@@ -0,0 +1,14 @@
+# Config file for Whitelist on_demand branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name and build number match the current production release.
+BRANCH_NAME=1.30
+BUILD=113131
+
+# on demand branch will only run when forced.
+SCHEDULER=None
diff --git a/lab_system/master/config/branch/next_release.cfg b/lab_system/master/config/branch/next_release.cfg
new file mode 100644
index 0000000..b7f1978
--- /dev/null
+++ b/lab_system/master/config/branch/next_release.cfg
@@ -0,0 +1,15 @@
+# Config file for branch for the next release
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use for the run. Change it to the next release branch.
+BRANCH_NAME=1.32
+
+# How often the tests will run
+SCHEDULER=15 */2 * * *
+
+USE_GN=1
diff --git a/lab_system/master/config/branch/on_demand.cfg b/lab_system/master/config/branch/on_demand.cfg
new file mode 100644
index 0000000..660ab59
--- /dev/null
+++ b/lab_system/master/config/branch/on_demand.cfg
@@ -0,0 +1,13 @@
+# Config file for on_demand branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in the run.
+BRANCH_NAME=1.26
+
+# on demand branch will only run when forced.
+SCHEDULER=None
diff --git a/lab_system/master/config/branch/release.cfg b/lab_system/master/config/branch/release.cfg
new file mode 100644
index 0000000..b15a507
--- /dev/null
+++ b/lab_system/master/config/branch/release.cfg
@@ -0,0 +1,15 @@
+# Config file for release branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use for the run. Change it to the latest release branch.
+BRANCH_NAME=1.31
+
+# How often the tests will run
+#SCHEDULER=31 0,8,16 * * *
+
+USE_GN=1
diff --git a/lab_system/master/config/buildbot/multi_master_config.json b/lab_system/master/config/buildbot/multi_master_config.json
new file mode 100644
index 0000000..3c94baa
--- /dev/null
+++ b/lab_system/master/config/buildbot/multi_master_config.json
@@ -0,0 +1,197 @@
+{
+    "home-platform-infra-14.cbf.corp.google.com": {
+        "database_ip": "173.194.227.201",
+        "database_name": "home_platform_infra_14",
+        "multi_master": false,
+        "slaves": {
+            "home-platform-infra-14.cbf.corp.google.com": [
+                "apps_launch_monitor",
+                "app_prelaunch",
+                "bluetooth_a2dp",
+                "build_upgrade",
+                "cloudcast",
+                "cpu_load",
+                "js_features"
+            ]
+        }
+    },
+    "home-platform-infra-16.cbf.corp.google.com": {
+        "database_ip": "173.194.227.201",
+        "database_name": "home_platform_infra_16",
+        "multi_master": false,
+        "slaves": {
+            "home-platform-infra-16.cbf.corp.google.com": [
+                "assistant_remote_casting"
+            ]
+        }
+    },
+    "home-lab-ci-master-1.mtv.corp.google.com": {
+        "database_ip": "173.194.227.201",
+        "multi_master": false,
+        "slaves": {
+            "home-lab-ci-slave-3.mtv.corp.google.com": [
+                "monitoring",
+                "uma_logging_wifi",
+                "webgl_conformance",
+                "factory"
+            ],
+            "home-lab-ci-slave-1.mtv.corp.google.com": [
+                "e2e_latency",
+                "eraser_for_hotword",
+                "hotword_detection_latency",
+                "hotword_samples_regression",
+                "hotwording",
+                "imax"
+            ],
+            "home-lab-ci-slave-2.mtv.corp.google.com": [
+                "multizone",
+                "octane",
+                "preview_test",
+                "tdls_test",
+                "uma_logging"
+            ],
+            "home-lab-ci-master-1.mtv.corp.google.com": [
+                "testingrepounittests"
+            ]
+        }
+    },
+    "cast-lab-infra-1.mtv.corp.google.com": {
+        "database_ip": "173.194.106.223",
+        "multi_master": false,
+        "slaves": {
+            "cast-lab-infra-1.mtv.corp.google.com": [
+                "cast_tv_unit_test",
+                "audiodefault_unit_test"
+            ]
+        }
+    },
+    "cast-lab-infra-20.mtv.corp.google.com": {
+        "database_ip": "173.194.106.223",
+        "database_name": "cast_lab_infra_20",
+        "multi_master": false,
+        "slaves": {
+            "cast-lab-infra-20.mtv.corp.google.com": [
+                "cast_tv_unit_test",
+                "audiodefault_unit_test"
+            ]
+        }
+    },
+    "cast-lab-infra-10.mtv.corp.google.com": {
+        "database_ip": "173.194.106.223",
+        "database_name": "unit_test",
+        "is_scheduler": true,
+        "multi_master": true
+    },
+    "cast-lab-infra-2.mtv.corp.google.com": {
+        "database_ip": "173.194.227.201",
+        "multi_master": false,
+        "slaves": {
+            "cast-lab-infra-2.mtv.corp.google.com": [
+                "android_mirroring",
+                "android_mirroring_uma"
+            ]
+        }
+    },
+    "cast-lab-infra-12.mtv.corp.google.com": {
+        "database_ip": "173.194.227.201",
+        "database_name": "cast_lab_infra_12",
+        "multi_master": false,
+        "slaves": {
+            "cast-lab-infra-12.mtv.corp.google.com": [
+                "android_mirroring",
+                "android_mirroring_uma"
+            ]
+        }
+    },
+    "cast-lab-infra-3.mtv.corp.google.com": {
+        "database_ip": "173.194.106.223",
+        "database_name": "cast_lab_infra_3_mtv_corp_google_com",
+        "multi_master": false,
+        "slaves": {
+            "cast-lab-infra-3.mtv.corp.google.com": [
+                "ota_installable"
+            ]
+        }
+    },
+    "cast-lab-infra-13.mtv.corp.google.com": {
+        "database_ip": "173.194.106.223",
+        "database_name": "cast_lab_infra_13",
+        "multi_master": false,
+        "slaves": {
+            "cast-lab-infra-13.mtv.corp.google.com": [
+                "ota_installable"
+            ]
+        }
+    },
+    "cast-lab-infra-7.mtv.corp.google.com": {
+        "database_ip": "173.194.106.223",
+        "database_name": "unit_test",
+        "is_scheduler": false,
+        "multi_master": true,
+        "slaves": {
+            "cast-lab-infra-7.mtv.corp.google.com": [
+                "ota_installable"
+            ]
+        }
+    },
+    "cast-lab-infra-17.mtv.corp.google.com": {
+        "database_ip": "173.194.106.223",
+        "database_name": "unit_test_prod",
+        "is_scheduler": false,
+        "multi_master": true,
+        "slaves": {
+            "cast-lab-infra-17.mtv.corp.google.com": [
+                "ota_installable"
+            ]
+        }
+    },
+    "cast-lab-infra-9.mtv.corp.google.com": {
+        "database_ip": "173.194.106.223",
+        "database_name": "unit_test",
+        "is_scheduler": true,
+        "multi_master": true
+    },
+    "cast-lab-infra-19.mtv.corp.google.com": {
+        "database_ip": "173.194.106.223",
+        "database_name": "unit_test_prod",
+        "is_scheduler": true,
+        "multi_master": true
+    },
+    "home-platform-infra-18.cbf.corp.google.com": {
+        "database_ip": "173.194.106.223",
+        "database_name": "home_platform_infra_18",
+        "multi_master": false,
+        "slaves": {
+            "home-platform-infra-18.cbf.corp.google.com": [
+                "atv_unit_test",
+                "cast_atv",
+                "androidthings_unit_test",
+                "cast_androidthings"
+            ]
+        }
+    },
+    "home-platform-infra-19.cbf.corp.google.com": {
+        "database_ip": "173.194.106.223",
+        "database_name": "home_platform_infra_19",
+        "multi_master": false,
+        "slaves": {
+            "home-platform-infra-19.cbf.corp.google.com": [
+                "cast_tv_unit_test",
+                "audiodefault_unit_test"
+            ]
+        }
+    },
+    "home-platform-infra-20.cbf.corp.google.com": {
+        "database_ip": "173.194.106.223",
+        "database_name": "home_platform_infra_20",
+        "multi_master": false,
+        "slaves": {
+            "home-platform-infra-20.cbf.corp.google.com": [
+                "atv_unit_test",
+                "cast_atv",
+                "androidthings_unit_test",
+                "cast_androidthings"
+            ]
+        }
+    }
+}
diff --git a/lab_system/master/config/device_monitoring/excluded_device_list.txt b/lab_system/master/config/device_monitoring/excluded_device_list.txt
new file mode 100644
index 0000000..be0cc4c
--- /dev/null
+++ b/lab_system/master/config/device_monitoring/excluded_device_list.txt
@@ -0,0 +1,34 @@
+100.107.130.239
+100.107.130.240
+100.107.130.241
+100.107.130.242
+100.107.131.10
+100.107.131.16
+100.107.131.19
+100.107.131.20
+100.107.131.214
+100.107.131.3
+100.107.131.4
+100.107.131.45
+100.107.131.70
+100.107.131.74
+100.107.131.75
+100.107.131.79
+100.107.131.8
+172.17.21.159
+172.17.21.167
+172.17.21.188
+172.17.21.197
+172.17.21.226
+172.17.21.234
+172.17.21.55
+172.17.21.71
+172.17.23.117
+172.17.23.172
+172.17.23.189
+172.17.23.210
+172.17.23.249
+172.17.23.28
+172.17.23.72
+172.17.23.80
+172.27.28.207
diff --git a/lab_system/master/config/device_monitoring/query_all_devices.sql b/lab_system/master/config/device_monitoring/query_all_devices.sql
new file mode 100644
index 0000000..5218523
--- /dev/null
+++ b/lab_system/master/config/device_monitoring/query_all_devices.sql
@@ -0,0 +1,2 @@
+--Query device with unit test device type
+select ip, type_name from ((select mac, ip, type_name, network_interface.device_id as id from network_interface join (select type_name, device.device_id from device where status != 3) as temp on network_interface.device_id=temp.device_id where ip != '' and type_name in %s) as foo join (select interface_mac, device_group_id from interface_group) as temp2 on mac=interface_mac) where ip not in %s;
diff --git a/lab_system/master/config/device_monitoring/query_unit_test_devices.sql b/lab_system/master/config/device_monitoring/query_unit_test_devices.sql
new file mode 100644
index 0000000..4622e0e
--- /dev/null
+++ b/lab_system/master/config/device_monitoring/query_unit_test_devices.sql
@@ -0,0 +1,2 @@
+--Query device with unit test device type
+select ip, type_name from ((select mac, ip, type_name, network_interface.device_id as id from network_interface join (select type_name, device.device_id from device where status != 3) as temp on network_interface.device_id=temp.device_id where ip != '' and type_name in %s) as foo join (select interface_mac, device_group_id from interface_group) as temp2 on mac=interface_mac and device_group_id=(select device_group_id from device_group where group_name='unit_test' limit 1)) where ip not in %s;
diff --git a/lab_system/master/config/device_monitoring/unit_test_type_list.txt b/lab_system/master/config/device_monitoring/unit_test_type_list.txt
new file mode 100644
index 0000000..1429059
--- /dev/null
+++ b/lab_system/master/config/device_monitoring/unit_test_type_list.txt
@@ -0,0 +1,19 @@
+audiodefault

+bubbletea

+chirp

+chorizo

+dragonfly

+earth

+eureka

+guabao

+hendrix

+lexx

+luther

+luther_b2

+mars_us

+mozart

+n00007

+pepperoni

+redwood

+suncake

+taroball

diff --git a/lab_system/master/config/device_type/assistantdefault.cfg b/lab_system/master/config/device_type/assistantdefault.cfg
new file mode 100644
index 0000000..bfb87e0
--- /dev/null
+++ b/lab_system/master/config/device_type/assistantdefault.cfg
@@ -0,0 +1,36 @@
+# Config file for the assistant reference device
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for assistant reference device test
+DEVICE_TYPE=assistantdefault
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=assistantdefault
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.assistant_device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Assistant
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=none
diff --git a/lab_system/master/config/device_type/astro_migration.cfg b/lab_system/master/config/device_type/astro_migration.cfg
new file mode 100644
index 0000000..2f0f9cf
--- /dev/null
+++ b/lab_system/master/config/device_type/astro_migration.cfg
@@ -0,0 +1,40 @@
+# Config file for astro_migration device type
+# Copied from estelle.cfg
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for tests
+DEVICE_TYPE=astro_migration
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=estelle
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=udm.device.astro_migration
+
+# Device class to use to control this device.
+DEVICE_CLASS=AstroMigration
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Platform version
+PLATFORM_VERSION=2.0
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=estelle-b3
diff --git a/lab_system/master/config/device_type/audiodefault.cfg b/lab_system/master/config/device_type/audiodefault.cfg
new file mode 100644
index 0000000..757f871
--- /dev/null
+++ b/lab_system/master/config/device_type/audiodefault.cfg
@@ -0,0 +1,36 @@
+# Config file for cast audio reference device
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for audio reference device test, usually audiodefault
+DEVICE_TYPE=audiodefault
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=audiodefault
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Device
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/cast_audio_reference
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/cast_audio_reference
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=tv
+
+# Unique repository name for this product
+REPO_ALIAS=cast_audio_reference
+
+# The board type to get the latest build
+BOARD_TYPE=none
diff --git a/lab_system/master/config/device_type/biggie.cfg b/lab_system/master/config/device_type/biggie.cfg
new file mode 100644
index 0000000..23a1d78
--- /dev/null
+++ b/lab_system/master/config/device_type/biggie.cfg
@@ -0,0 +1,39 @@
+# Config file for Biggie device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for tests
+DEVICE_TYPE=biggie
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=biggie
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.assistant_device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Assistant
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Platform version
+PLATFORM_VERSION=2.0
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=biggie-b1
diff --git a/lab_system/master/config/device_type/biggie_b3.cfg b/lab_system/master/config/device_type/biggie_b3.cfg
new file mode 100644
index 0000000..2741148
--- /dev/null
+++ b/lab_system/master/config/device_type/biggie_b3.cfg
@@ -0,0 +1,39 @@
+# Config file for Biggie device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for tests
+DEVICE_TYPE=biggie_b3
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=biggie
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.assistant_device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Assistant
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Platform version
+PLATFORM_VERSION=2.0
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=biggie-b3
diff --git a/lab_system/master/config/device_type/caprica.cfg b/lab_system/master/config/device_type/caprica.cfg
new file mode 100644
index 0000000..3704413
--- /dev/null
+++ b/lab_system/master/config/device_type/caprica.cfg
@@ -0,0 +1,23 @@
+# Config file for caprica device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for eureka tests, usually eureka
+DEVICE_TYPE=caprica
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=caprica
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Device
+
+# The board type to get the latest build
+BOARD_TYPE=caprica-b3
diff --git a/lab_system/master/config/device_type/chirp.cfg b/lab_system/master/config/device_type/chirp.cfg
new file mode 100644
index 0000000..ad42ae2
--- /dev/null
+++ b/lab_system/master/config/device_type/chirp.cfg
@@ -0,0 +1,36 @@
+# Config file for chirp device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for eureka tests, usually eureka
+DEVICE_TYPE=chirp
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=pineapple
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.assistant_device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Assistant
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=chirp-b1
diff --git a/lab_system/master/config/device_type/chirp_b3.cfg b/lab_system/master/config/device_type/chirp_b3.cfg
new file mode 100644
index 0000000..79ec12c
--- /dev/null
+++ b/lab_system/master/config/device_type/chirp_b3.cfg
@@ -0,0 +1,36 @@
+# Config file for chirp device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for eureka tests, usually eureka
+DEVICE_TYPE=chirp_b3
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=pineapple
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.assistant_device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Assistant
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=chirp-b3
diff --git a/lab_system/master/config/device_type/earth.cfg b/lab_system/master/config/device_type/earth.cfg
new file mode 100644
index 0000000..332d187
--- /dev/null
+++ b/lab_system/master/config/device_type/earth.cfg
@@ -0,0 +1,36 @@
+# Config file for earth device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for eureka tests, usually eureka
+DEVICE_TYPE=earth
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=chorizo
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Device
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=earth-b3
diff --git a/lab_system/master/config/device_type/elaine.cfg b/lab_system/master/config/device_type/elaine.cfg
new file mode 100644
index 0000000..d027d1d
--- /dev/null
+++ b/lab_system/master/config/device_type/elaine.cfg
@@ -0,0 +1,39 @@
+# Config file for elaine device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for tests
+DEVICE_TYPE=elaine
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=elaine
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.assistant_device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Assistant
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Platform version
+PLATFORM_VERSION=2.0
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=elaine-b1
diff --git a/lab_system/master/config/device_type/estelle.cfg b/lab_system/master/config/device_type/estelle.cfg
new file mode 100644
index 0000000..e453dd1
--- /dev/null
+++ b/lab_system/master/config/device_type/estelle.cfg
@@ -0,0 +1,39 @@
+# Config file for estelle device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for tests
+DEVICE_TYPE=estelle
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=estelle
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.assistant_device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Assistant
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Platform version
+PLATFORM_VERSION=2.0
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=estelle-p0
diff --git a/lab_system/master/config/device_type/eureka.cfg b/lab_system/master/config/device_type/eureka.cfg
new file mode 100644
index 0000000..ec0a10f
--- /dev/null
+++ b/lab_system/master/config/device_type/eureka.cfg
@@ -0,0 +1,36 @@
+# Config file for eureka device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for eureka tests, usually eureka
+DEVICE_TYPE=eureka
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=anchovy
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Device
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# Board type
+BOARD_TYPE=eureka-b1
diff --git a/lab_system/master/config/device_type/hendrix.cfg b/lab_system/master/config/device_type/hendrix.cfg
new file mode 100644
index 0000000..cc71a7a
--- /dev/null
+++ b/lab_system/master/config/device_type/hendrix.cfg
@@ -0,0 +1,36 @@
+# Config file for earth device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for eureka tests, usually eureka
+DEVICE_TYPE=hendrix
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=pepperoni
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.chromecast_audio
+
+# Device class to use to control this device.
+DEVICE_CLASS=ChromecastAudio
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=hendrix-b3
diff --git a/lab_system/master/config/device_type/joplin.cfg b/lab_system/master/config/device_type/joplin.cfg
new file mode 100644
index 0000000..b75ad34
--- /dev/null
+++ b/lab_system/master/config/device_type/joplin.cfg
@@ -0,0 +1,39 @@
+# Config file for joplin device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for tests
+DEVICE_TYPE=joplin
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=mushroom
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.assistant_device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Assistant
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Platform version
+PLATFORM_VERSION=2.0
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=joplin-b1
diff --git a/lab_system/master/config/device_type/lexx.cfg b/lab_system/master/config/device_type/lexx.cfg
new file mode 100644
index 0000000..0ebc5a1
--- /dev/null
+++ b/lab_system/master/config/device_type/lexx.cfg
@@ -0,0 +1,39 @@
+# Config file for lexx device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for tests
+DEVICE_TYPE=lexx
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=salami
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Device
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Platform version
+PLATFORM_VERSION=2.0
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=lexx-b3
diff --git a/lab_system/master/config/device_type/luther.cfg b/lab_system/master/config/device_type/luther.cfg
new file mode 100644
index 0000000..fe0c076
--- /dev/null
+++ b/lab_system/master/config/device_type/luther.cfg
@@ -0,0 +1,39 @@
+# Config file for luther device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for tests
+DEVICE_TYPE=luther_b2
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=steak
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Device
+
+# Path to build installer.
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Platform version
+PLATFORM_VERSION=4.0
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=luther-b2
diff --git a/lab_system/master/config/device_type/luther_b2.cfg b/lab_system/master/config/device_type/luther_b2.cfg
new file mode 100644
index 0000000..fe0c076
--- /dev/null
+++ b/lab_system/master/config/device_type/luther_b2.cfg
@@ -0,0 +1,39 @@
+# Config file for luther device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for tests
+DEVICE_TYPE=luther_b2
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=steak
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Device
+
+# Path to build installer.
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Platform version
+PLATFORM_VERSION=4.0
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=luther-b2
diff --git a/lab_system/master/config/device_type/newman.cfg b/lab_system/master/config/device_type/newman.cfg
new file mode 100644
index 0000000..8b77ea0
--- /dev/null
+++ b/lab_system/master/config/device_type/newman.cfg
@@ -0,0 +1,25 @@
+# Config file for newman device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for tests
+DEVICE_TYPE=newman
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=newman
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.assistant_device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Assistant
+
+# Approved wifi channels by nation for prince,
+# that differ from standard channels available in that country.
+CA=36,40,44,48,52,56,60,64,100,104,108,112,116,120,124,128,132,136,140,144,149,153,157,161,165
+KR=36,40,44,48,52,56,60,64,100,104,108,112,116,120,124,128,132,136,140,144,149,153,157,161,165
diff --git a/lab_system/master/config/device_type/prince.cfg b/lab_system/master/config/device_type/prince.cfg
new file mode 100644
index 0000000..a4bb088
--- /dev/null
+++ b/lab_system/master/config/device_type/prince.cfg
@@ -0,0 +1,27 @@
+# Config file for Prince device type
+#
+# Do not modify the property names
+#
+# PROPERTIES
+
+# name to be used for tests
+DEVICE_TYPE=prince
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=prince
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.assistant_device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Assistant
+
+# Approved wifi channels by nation for prince,
+# that differ from standard channels available in that country
+FR=36,40,44,48,52,56,60,64,100,104,108,112,116,120,124,128,132,136,140,149,153,157,161,165
+JP=36,40,44,48,52,56,60,64,100,104,108,112,116,120,124,128,132,136,140,144
+IT=36,40,44,48,52,56,60,64,100,104,108,112,116,120,124,128,132,136,140,149,153,157,161,165
+GB=36,40,44,48,52,56,60,64,100,104,108,112,116,120,124,128,132,136,140,149,153,157,161,165
+IE=36,40,44,48,52,56,60,64,100,104,108,112,116,120,124,128,132,136,140,149,153,157,161,165
+PL=36,40,44,48,52,56,60,64,100,104,108,112,116,120,124,128,132,136,140,149,153,157,161,165
+KR=36,40,44,48,52,56,60,64,100,104,108,112,116,120,124,128,132,136,140,144,149,153,157,161,165
diff --git a/lab_system/master/config/device_type/sabrina.cfg b/lab_system/master/config/device_type/sabrina.cfg
new file mode 100644
index 0000000..3dae473
--- /dev/null
+++ b/lab_system/master/config/device_type/sabrina.cfg
@@ -0,0 +1,20 @@
+# Config file for sabrina device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used as device type
+DEVICE_TYPE=sabrina
+
+# product name
+PRODUCT_NAME=sabrina
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.android
+
+# Device class to use to control this device.
+DEVICE_CLASS=Android
diff --git a/lab_system/master/config/device_type/spencer.cfg b/lab_system/master/config/device_type/spencer.cfg
new file mode 100644
index 0000000..847c44c
--- /dev/null
+++ b/lab_system/master/config/device_type/spencer.cfg
@@ -0,0 +1,20 @@
+# Config file for Spencer device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used as device type
+DEVICE_TYPE=spencer
+
+# product name
+PRODUCT_NAME=spencer
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=udm.device.nest_iot_device
+
+# Device class to use to control this device.
+DEVICE_CLASS=SpencerDevice
diff --git a/lab_system/master/config/device_type/valens.cfg b/lab_system/master/config/device_type/valens.cfg
new file mode 100644
index 0000000..2b1ffb8
--- /dev/null
+++ b/lab_system/master/config/device_type/valens.cfg
@@ -0,0 +1,39 @@
+# Config file for valens device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for tests
+DEVICE_TYPE=valens
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=valens
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.assistant_device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Assistant
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Platform version
+PLATFORM_VERSION=2.0
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=valens-p2
diff --git a/lab_system/master/config/device_type/vento.cfg b/lab_system/master/config/device_type/vento.cfg
new file mode 100644
index 0000000..01c4a8d
--- /dev/null
+++ b/lab_system/master/config/device_type/vento.cfg
@@ -0,0 +1,39 @@
+# Config file for vento device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for tests
+DEVICE_TYPE=vento
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=vento
+
+# Device module which contains class to control this device.
+DEVICE_MODULE=eurtest.device.assistant_device
+
+# Device class to use to control this device.
+DEVICE_CLASS=Assistant
+
+# Path to build installer
+BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/ota_update.py
+
+# Platform version
+PLATFORM_VERSION=1.0
+
+# Cloud Storage bucket root e.g. gs://gtv-eureka/internal
+BUILD_BUCKET_ROOT=gtv-eureka/internal
+UNIT_TEST_BUCKET_ROOT=chromecast-unit-test/internal
+
+# build project name e.g. eureka is "chromecast"
+BUILD_PROJECT_NAME=chromecast
+
+# Unique repository name for this product
+REPO_ALIAS=internal
+
+# The board type to get the latest build
+BOARD_TYPE=vento-p0
diff --git a/lab_system/master/config/groups/acer_core_i5_chrome_mirroring_win_eureka_master.cfg b/lab_system/master/config/groups/acer_core_i5_chrome_mirroring_win_eureka_master.cfg
new file mode 100644
index 0000000..83e4930
--- /dev/null
+++ b/lab_system/master/config/groups/acer_core_i5_chrome_mirroring_win_eureka_master.cfg
@@ -0,0 +1,17 @@
+# Config file for acer_staging slave
+
+# PROPERTIES
+SPECIFIC_DEVICE_IP=192.168.0.106
+SELF_IP=172.17.23.194
+HOME_DIR=C:/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver.exe
+CHROME_ZIP=chrome-win32.zip
+CHROME_PATH=chrome-win32
+PLATFORM=win
+
+# Mirroring test chrome build
+CHROME_CHANNEL=stable
+
+# Mirroring test extension flavor
+EXTENSION_FLAVOR=canary
diff --git a/lab_system/master/config/groups/android_mirroring_cast_tv_1.cfg b/lab_system/master/config/groups/android_mirroring_cast_tv_1.cfg
new file mode 100644
index 0000000..8f3c2f9
--- /dev/null
+++ b/lab_system/master/config/groups/android_mirroring_cast_tv_1.cfg
@@ -0,0 +1,10 @@
+# Config file for recording laptop
+
+# PROPERTIES
+SELF_IP=100.107.130.19
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=192.168.1.54
+RECORDER_DEVICE_TYPE='video_recorder'
+
diff --git a/lab_system/master/config/groups/android_mirroring_recording_1.cfg b/lab_system/master/config/groups/android_mirroring_recording_1.cfg
new file mode 100644
index 0000000..6fa6394
--- /dev/null
+++ b/lab_system/master/config/groups/android_mirroring_recording_1.cfg
@@ -0,0 +1,19 @@
+# Config file for recording laptop
+
+# PROPERTIES
+SELF_IP=100.107.131.0
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=192.168.1.6
+RECORDER_DEVICE_TYPE='video_recorder'
+
+# GmsCore file name
+GMSCORE_APK=GmsCore.apk
+
+# GMSCore branch and target defining builds to fetch:
+GMSCORE_BRANCH=ub-gcore-saga-release
+GMSCORE_TARGET=GmsCore
+
+# GMSCORE Build Id
+BID=2250156
diff --git a/lab_system/master/config/groups/android_mirroring_recording_2.cfg b/lab_system/master/config/groups/android_mirroring_recording_2.cfg
new file mode 100644
index 0000000..c6649e9
--- /dev/null
+++ b/lab_system/master/config/groups/android_mirroring_recording_2.cfg
@@ -0,0 +1,19 @@
+# Config file for recording laptop
+
+# PROPERTIES
+SELF_IP=100.115.4.74
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=192.168.0.103
+RECORDER_DEVICE_TYPE='video_recorder'
+
+# GmsCore file name
+GMSCORE_APK=GmsCore.apk
+
+# GMSCore branch and target defining builds to fetch:
+GMSCORE_BRANCH=ub-gcore-saga-release
+GMSCORE_TARGET=GmsCore
+
+# GMSCORE Build Id
+BID=2250156
diff --git a/lab_system/master/config/groups/android_mirroring_recording_3.cfg b/lab_system/master/config/groups/android_mirroring_recording_3.cfg
new file mode 100644
index 0000000..d297082
--- /dev/null
+++ b/lab_system/master/config/groups/android_mirroring_recording_3.cfg
@@ -0,0 +1,19 @@
+# Config file for recording laptop
+
+# PROPERTIES
+SELF_IP=100.115.4.73
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=192.168.0.3
+RECORDER_DEVICE_TYPE='video_recorder'
+
+# GmsCore file name
+GMSCORE_APK=GmsCore.apk
+
+# GMSCore branch and target defining builds to fetch:
+GMSCORE_BRANCH=ub-gcore-saga-release
+GMSCORE_TARGET=GmsCore
+
+# GMSCORE Build Id
+BID=2250156
diff --git a/lab_system/master/config/groups/android_mirroring_recording_4.cfg b/lab_system/master/config/groups/android_mirroring_recording_4.cfg
new file mode 100644
index 0000000..574aba8
--- /dev/null
+++ b/lab_system/master/config/groups/android_mirroring_recording_4.cfg
@@ -0,0 +1,9 @@
+# Config file for laptop connecting to OEM devices
+
+# PROPERTIES
+SELF_IP=100.107.131.181
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=192.168.0.4
+RECORDER_DEVICE_TYPE='video_recorder'
diff --git a/lab_system/master/config/groups/android_mirroring_uma.cfg b/lab_system/master/config/groups/android_mirroring_uma.cfg
new file mode 100644
index 0000000..46d753b
--- /dev/null
+++ b/lab_system/master/config/groups/android_mirroring_uma.cfg
@@ -0,0 +1,11 @@
+# Config file for recording laptop
+
+# PROPERTIES
+SELF_IP=100.115.4.111
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=192.168.0.4
+REMOTE_LOG_DIR=/Users/gtvchrome/results/uma
+# TODO(kevinfei): remove IPA_DIR once iOS test has its dedicated setup
+IPA_DIR=/Users/gtvchrome/ipa
diff --git a/lab_system/master/config/groups/audio_mirroring_nexus_1.cfg b/lab_system/master/config/groups/audio_mirroring_nexus_1.cfg
new file mode 100644
index 0000000..5b84a2c
--- /dev/null
+++ b/lab_system/master/config/groups/audio_mirroring_nexus_1.cfg
@@ -0,0 +1,38 @@
+# Config file for laptop connecting to OEM devices
+
+# PROPERTIES
+SELF_IP=100.107.130.143
+HOME_DIR=/home/gtvchrome
+AUDIO_RECORDER_IP=100.107.131.179
+AUDIO_RECORDER_HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=192.168.0.103
+RECORDER_DEVICE_TYPE='audio_recorder'
+
+# GmsCore file name
+GMSCORE_APK=GmsCore.apk
+
+# GMSCore branch and target defining builds to fetch:
+GMSCORE_BRANCH=ub-gcore-saga-release
+GMSCORE_TARGET=GmsCore
+
+# GMSCORE Build Id
+BID=2250156
+
+# Sender device
+ANDROID_DEVICE_NAME=nexus5
+# Audio Reference
+REMOTE_AUDIO_REF_DIR=/media/media/www/audio_mirroring
+# Audio score file name
+AUDIO_SCORE_FILE_NAME=audio_score.txt
+# Audio alignment analysis file name
+AUDIO_ALIGNMENT_FILE_NAME=audio_missing_delay.json
+# Recorded audio file name
+RECORDED_AUDIO_FILE_NAME=audio_all_chn.wav
+# Original reference audio file name
+ORIGINAL_AUDIO_FILE_NAME=sunshine-reference_delay_000.wav
+# Find highest audio score binary
+FIND_MAX_AUDIO_SCORE=test/report/audio_mirroring/find_max_audio_score.py
+# Upload test result binary
+UPLOAD_TEST_RESULT=test/report/audio_mirroring/upload_test_result.py
diff --git a/lab_system/master/config/groups/audio_mirroring_nexus_2.cfg b/lab_system/master/config/groups/audio_mirroring_nexus_2.cfg
new file mode 100644
index 0000000..5ca6a7e
--- /dev/null
+++ b/lab_system/master/config/groups/audio_mirroring_nexus_2.cfg
@@ -0,0 +1,38 @@
+# Config file for laptop connecting to OEM devices
+
+# PROPERTIES
+SELF_IP=172.17.21.224
+HOME_DIR=/home/gtvchrome
+AUDIO_RECORDER_IP=172.17.21.219
+AUDIO_RECORDER_HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=10.0.1.4
+RECORDER_DEVICE_TYPE='audio_recorder'
+
+# GmsCore file name
+GMSCORE_APK=GmsCore.apk
+
+# GMSCore branch and target defining builds to fetch:
+GMSCORE_BRANCH=ub-gcore-saga-release
+GMSCORE_TARGET=GmsCore
+
+# GMSCORE Build Id
+BID=2250156
+
+# Sender device
+ANDROID_DEVICE_NAME=nexus5
+# Audio Reference
+REMOTE_AUDIO_REF_DIR=/media/media/www/audio_mirroring
+# Audio score file name
+AUDIO_SCORE_FILE_NAME=audio_score.txt
+# Audio alignment analysis file name
+AUDIO_ALIGNMENT_FILE_NAME=audio_missing_delay.json
+# Recorded audio file name
+RECORDED_AUDIO_FILE_NAME=audio_all_chn.wav
+# Original reference audio file name
+ORIGINAL_AUDIO_FILE_NAME=sunshine-reference_delay_000.wav
+# Find highest audio score binary
+FIND_MAX_AUDIO_SCORE=test/report/audio_mirroring/find_max_audio_score.py
+# Upload test result binary
+UPLOAD_TEST_RESULT=test/report/audio_mirroring/upload_test_result.py
diff --git a/lab_system/master/config/groups/bot.cfg b/lab_system/master/config/groups/bot.cfg
new file mode 100644
index 0000000..81622af
--- /dev/null
+++ b/lab_system/master/config/groups/bot.cfg
@@ -0,0 +1,9 @@
+# Swarming configuration
+ISOLATE_SERVER=cast-isolate.appspot.com
+SWARMING_SERVER=cast-swarming.appspot.com
+# Swarming Bot Configuration
+BOT_OS=Ubuntu
+BOT_POOL=unit_test
+BOT_HARD_TIMEOUT=10800
+BOT_IO_TIMEOUT=10800
+REMOTE_HOME_DIR=/home/gtvchrome/repositories
diff --git a/lab_system/master/config/groups/cast_androidthings.cfg b/lab_system/master/config/groups/cast_androidthings.cfg
new file mode 100644
index 0000000..92aa585
--- /dev/null
+++ b/lab_system/master/config/groups/cast_androidthings.cfg
@@ -0,0 +1,10 @@
+# Android Things test properties
+
+USER_NAME=gtvchrome
+REMOTE_REPO_ROOT=/home/gtvchrome/repositories
+REMOTE_TEST_ROOT=/home/gtvchrome/repositories/HEAD
+CHROMIUM_SRC=chromium/src
+ANDROID_TEST_RUNNER_PATH=build/android/test_runner.py
+REMOTE_OTA_ZIP_PATH=/home/gtvchrome/repositories/test_deps_claimed.zip
+REMOTE_TEMP_DIR=/home/gtvchrome/atv_tmp
+REMOTE_RESULT_DIR=/home/gtvchrome/results
diff --git a/lab_system/master/config/groups/cast_mdns.cfg b/lab_system/master/config/groups/cast_mdns.cfg
new file mode 100644
index 0000000..05cc376
--- /dev/null
+++ b/lab_system/master/config/groups/cast_mdns.cfg
@@ -0,0 +1,7 @@
+# Config file for cast_mdns test
+
+# PROPERTIES
+SELF_IP=172.17.23.200
+HOME_DIR=/home/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
diff --git a/lab_system/master/config/groups/chrome_sdk_end_to_end_v2.cfg b/lab_system/master/config/groups/chrome_sdk_end_to_end_v2.cfg
new file mode 100644
index 0000000..eec9439
--- /dev/null
+++ b/lab_system/master/config/groups/chrome_sdk_end_to_end_v2.cfg
@@ -0,0 +1,25 @@
+# Config file for chrome_sdk_end_to_end_v2 group
+
+# PROPERTIES
+REMOTE_TEST_FILES_FOLDER=/var/www/gtv/bin
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.chrome.end_to_end_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastChromeEndToEndTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+# Location of test binary
+TEST_BINARY_FILE=gs://test_logs/test_binaries/test_bin.zip
+
+# Account information used to download internal cast extension
+ACCOUNT_FILE=/usr/local/google/staging/fraggle_rock/chrome_sdk/account.txt
+
+# Public keys for different cast extensions
+EXTENSION_PUBLIC_KEYS_FILE=/usr/local/google/staging/fraggle_rock/chrome_sdk/public_keys.json
diff --git a/lab_system/master/config/groups/fugu_unit_test.cfg b/lab_system/master/config/groups/fugu_unit_test.cfg
new file mode 100644
index 0000000..07009ae
--- /dev/null
+++ b/lab_system/master/config/groups/fugu_unit_test.cfg
@@ -0,0 +1,10 @@
+# Android TV unit test properties
+
+USER_NAME=gtvchrome
+REMOTE_REPO_ROOT=/home/gtvchrome/repositories
+REMOTE_TEST_ROOT=/home/gtvchrome/repositories/HEAD
+CHROMIUM_SRC=chromium/src
+ANDROID_TEST_RUNNER_PATH=build/android/test_runner.py
+REMOTE_OTA_ZIP_PATH=/home/gtvchrome/repositories/test_deps_claimed.zip
+REMOTE_TEMP_DIR=/home/gtvchrome/atv_tmp
+REMOTE_RESULT_DIR=/home/gtvchrome/results
diff --git a/lab_system/master/config/groups/hp_core_i5_chrome_mirroring_win_eureka_master.cfg b/lab_system/master/config/groups/hp_core_i5_chrome_mirroring_win_eureka_master.cfg
new file mode 100644
index 0000000..73f0454
--- /dev/null
+++ b/lab_system/master/config/groups/hp_core_i5_chrome_mirroring_win_eureka_master.cfg
@@ -0,0 +1,17 @@
+# Config file for acer_staging slave
+
+# PROPERTIES
+SPECIFIC_DEVICE_IP=192.168.0.244
+SELF_IP=172.17.23.78
+HOME_DIR=C:/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver.exe
+CHROME_ZIP=chrome-win32.zip
+CHROME_PATH=chrome-win32
+PLATFORM=win
+
+# Mirroring test chrome build
+CHROME_CHANNEL=beta
+
+# Mirroring test extension flavor
+EXTENSION_FLAVOR=canary
diff --git a/lab_system/master/config/groups/macbookair_haswell_chrome_mirroring_mac_eureka_master.cfg b/lab_system/master/config/groups/macbookair_haswell_chrome_mirroring_mac_eureka_master.cfg
new file mode 100644
index 0000000..6d5fa0f
--- /dev/null
+++ b/lab_system/master/config/groups/macbookair_haswell_chrome_mirroring_mac_eureka_master.cfg
@@ -0,0 +1,17 @@
+# Config file for macbookair_staging slave
+
+# PROPERTIES
+SPECIFIC_DEVICE_IP=192.168.0.102
+SELF_IP=172.17.23.86
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver
+CHROME_ZIP=chrome-mac.zip
+CHROME_PATH=chrome-mac
+PLATFORM=mac
+
+# Mirroring test chrome build
+CHROME_CHANNEL=beta
+
+# Mirroring test extension flavor
+EXTENSION_FLAVOR=canary
diff --git a/lab_system/master/config/groups/macbookpro13_core_i5_chrome_mirroring_mac_eureka_master.cfg b/lab_system/master/config/groups/macbookpro13_core_i5_chrome_mirroring_mac_eureka_master.cfg
new file mode 100644
index 0000000..74d6fbe
--- /dev/null
+++ b/lab_system/master/config/groups/macbookpro13_core_i5_chrome_mirroring_mac_eureka_master.cfg
@@ -0,0 +1,21 @@
+# Config file for 
+# macbookpro13_core_i5_chrome_mirroring_mac_eureka_master slave
+
+# PROPERTIES
+SPECIFIC_DEVICE_IP=192.168.0.103
+SELF_IP=172.17.23.84
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver
+CHROME_ZIP=chrome-mac.zip
+CHROME_PATH=chrome-mac
+PLATFORM=mac
+TEST_CONFIG=continuous-tests/lab_system/master/config/tests/chrome_mirroring_benchmark/chrome_mirroring_benchmark_test_conf.json
+RESULTS_DIR=results
+TEST_NAME=chrome_mirroring_mac
+
+# Mirroring test chrome build
+CHROME_CHANNEL=beta
+
+# Mirroring test extension flavor
+EXTENSION_FLAVOR=canary
diff --git a/lab_system/master/config/groups/macbookpro13_core_i5_retina_chrome_mirroring_mac_eureka_master.cfg b/lab_system/master/config/groups/macbookpro13_core_i5_retina_chrome_mirroring_mac_eureka_master.cfg
new file mode 100644
index 0000000..b0561d0
--- /dev/null
+++ b/lab_system/master/config/groups/macbookpro13_core_i5_retina_chrome_mirroring_mac_eureka_master.cfg
@@ -0,0 +1,18 @@
+# Config file for 
+# macbookpro13_core_i5_retina_chrome_mirroring_mac_eureka_master slave
+
+# PROPERTIES
+SPECIFIC_DEVICE_IP=192.168.0.101
+SELF_IP=172.17.23.56
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver
+CHROME_ZIP=chrome-mac.zip
+CHROME_PATH=chrome-mac
+PLATFORM=mac
+
+# Mirroring test chrome build
+CHROME_CHANNEL=beta
+
+# Mirroring test extension flavor
+EXTENSION_FLAVOR=canary
diff --git a/lab_system/master/config/groups/nexus_player_recording.cfg b/lab_system/master/config/groups/nexus_player_recording.cfg
new file mode 100644
index 0000000..7e1feaf
--- /dev/null
+++ b/lab_system/master/config/groups/nexus_player_recording.cfg
@@ -0,0 +1,9 @@
+# Config file for Nexus Player recording laptop
+
+# PROPERTIES
+SELF_IP=100.107.131.180
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=E34884C7
+RECORDER_DEVICE_TYPE='video_recorder'
diff --git a/lab_system/master/config/groups/remote_display_ios_1.cfg b/lab_system/master/config/groups/remote_display_ios_1.cfg
new file mode 100644
index 0000000..7da5b6d
--- /dev/null
+++ b/lab_system/master/config/groups/remote_display_ios_1.cfg
@@ -0,0 +1,9 @@
+# Config file for recording laptop
+
+# PROPERTIES
+SELF_IP=100.107.130.67
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=192.168.0.4
+IPA_DIR=/Users/gtvchrome/ipa
diff --git a/lab_system/master/config/groups/remote_display_ios_latency_1.cfg b/lab_system/master/config/groups/remote_display_ios_latency_1.cfg
new file mode 100644
index 0000000..0c4a4ba
--- /dev/null
+++ b/lab_system/master/config/groups/remote_display_ios_latency_1.cfg
@@ -0,0 +1,9 @@
+# Config file for recording laptop
+
+# PROPERTIES
+SELF_IP=172.17.21.219
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=10.0.1.7
+IPA_DIR=/Users/gtvchrome/ipa
diff --git a/lab_system/master/config/groups/remote_display_recording_1.cfg b/lab_system/master/config/groups/remote_display_recording_1.cfg
new file mode 100644
index 0000000..e21a5ae
--- /dev/null
+++ b/lab_system/master/config/groups/remote_display_recording_1.cfg
@@ -0,0 +1,19 @@
+# Config file for recording laptop
+
+# PROPERTIES
+SELF_IP=100.107.131.0
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=192.168.1.6
+RECORDER_DEVICE_TYPE='video_recorder'
+
+# GmsCore file name
+GMSCORE_APK=GmsCore.apk
+
+# GMSCore branch and target defining builds to fetch:
+GMSCORE_BRANCH=ub-gcore-saga-release
+GMSCORE_TARGET=GmsCore
+
+# Build Id
+BID=2250156
diff --git a/lab_system/master/config/groups/remote_display_recording_2.cfg b/lab_system/master/config/groups/remote_display_recording_2.cfg
new file mode 100644
index 0000000..34bfa07
--- /dev/null
+++ b/lab_system/master/config/groups/remote_display_recording_2.cfg
@@ -0,0 +1,19 @@
+# Config file for recording laptop
+
+# PROPERTIES
+SELF_IP=100.107.130.131
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=192.168.0.103
+RECORDER_DEVICE_TYPE='video_recorder'
+
+# GmsCore file name
+GMSCORE_APK=GmsCore.apk
+
+# GMSCore branch and target defining builds to fetch:
+GMSCORE_BRANCH=ub-gcore-saga-release
+GMSCORE_TARGET=GmsCore
+
+# Build Id
+BID=2250156
diff --git a/lab_system/master/config/groups/remote_display_recording_3.cfg b/lab_system/master/config/groups/remote_display_recording_3.cfg
new file mode 100644
index 0000000..2cae7b2
--- /dev/null
+++ b/lab_system/master/config/groups/remote_display_recording_3.cfg
@@ -0,0 +1,19 @@
+# Config file for recording laptop
+
+# PROPERTIES
+SELF_IP=100.107.130.36
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=192.168.0.3
+RECORDER_DEVICE_TYPE='video_recorder'
+
+# GmsCore file name
+GMSCORE_APK=GmsCore.apk
+
+# GMSCore branch and target defining builds to fetch:
+GMSCORE_BRANCH=ub-gcore-saga-release
+GMSCORE_TARGET=GmsCore
+
+# Build Id
+BID=2250156
diff --git a/lab_system/master/config/groups/remote_display_recording_4.cfg b/lab_system/master/config/groups/remote_display_recording_4.cfg
new file mode 100644
index 0000000..e7ef280
--- /dev/null
+++ b/lab_system/master/config/groups/remote_display_recording_4.cfg
@@ -0,0 +1,9 @@
+# Config file for recording laptop
+
+# PROPERTIES
+SELF_IP=100.107.131.181
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+SPECIFIC_DEVICE_IP=192.168.0.4
+RECORDER_DEVICE_TYPE='video_recorder'
diff --git a/lab_system/master/config/ios/ios_device_type.json b/lab_system/master/config/ios/ios_device_type.json
new file mode 100644
index 0000000..924675b
--- /dev/null
+++ b/lab_system/master/config/ios/ios_device_type.json
@@ -0,0 +1,26 @@
+{
+    "iPad4,4": {
+        "IpaName": "GLES3.ipa",
+        "ProductName": "iPadMini2WiFi",
+        "UnityAppName": "com.google.cast.test.unityplugin",
+        "UnityAppType": "GLES3"
+    },
+    "iPhone5,2": {
+        "IpaName": "GLES2.ipa",
+        "ProductName": "iPhone5",
+        "UnityAppName": "com.google.cast.test.unityplugin",
+        "UnityAppType": "GLES2"
+    },
+    "iPad5,3": {
+        "IpaName": "METAL.ipa",
+        "ProductName": "iPhone6s",
+        "UnityAppName": "com.google.cast.test.unityplugin",
+        "UnityAppType": "METAL"
+    },
+    "iPhone8,1": {
+        "IpaName": "METAL.ipa",
+        "ProductName": "iPhone6s",
+        "UnityAppName": "com.google.cast.test.unityplugin",
+        "UnityAppType": "METAL"
+    }
+}
diff --git a/lab_system/master/config/isolate/unit_test.isolate.template b/lab_system/master/config/isolate/unit_test.isolate.template
new file mode 100644
index 0000000..d2d6e01
--- /dev/null
+++ b/lab_system/master/config/isolate/unit_test.isolate.template
@@ -0,0 +1,19 @@
+{
+  'variables': {
+    'files': [
+      {% for path in mandatory_paths %}'{{ relative_path }}/{{ path }}/',
+      {% endfor %}],
+  },
+  'conditions': [
+    ['OS=="{{ os_platform }}"', {
+      'variables': {
+        'command': [
+          '{{ relative_path }}/continuous-tests/cq/scripts/tools/run_recipe.py',
+          '{{ valid_test_name }}',
+          '--eureka-root={{ remote_home_dir }}',
+          '--properties={% raw %}{{% endraw %}{% for key, value in props.iteritems() %}"{{ key }}": "{{ value }}", {% endfor %}"recipe_tmp_dir": "{{ recipe_tmp_dir }}"{% raw %}}{% endraw %}'
+        ],
+      },
+    }],
+  ],
+}
diff --git a/lab_system/master/config/kir/branch/master.cfg b/lab_system/master/config/kir/branch/master.cfg
new file mode 100644
index 0000000..0cd1393
--- /dev/null
+++ b/lab_system/master/config/kir/branch/master.cfg
@@ -0,0 +1,10 @@
+# Config file for master branch
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in the run
+BRANCH_NAME=unfork_m37
diff --git a/lab_system/master/config/kir/branch/release.cfg b/lab_system/master/config/kir/branch/release.cfg
new file mode 100644
index 0000000..a31ad9c
--- /dev/null
+++ b/lab_system/master/config/kir/branch/release.cfg
@@ -0,0 +1,10 @@
+# Config file for release branches
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# while buildbot is running
+#
+# PROPERTIES
+
+# Branch name to use in the run. Change it to the latest release branch.
+BRANCH_NAME=1.9
diff --git a/lab_system/master/config/kir/dashboard/dashboard.cfg b/lab_system/master/config/kir/dashboard/dashboard.cfg
new file mode 100755
index 0000000..52721da
--- /dev/null
+++ b/lab_system/master/config/kir/dashboard/dashboard.cfg
@@ -0,0 +1,1112 @@
+{
+   "Chrome SDK v2 Flinging Test - Mac_Canary_Canary_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_canary_canary_release' AND (test_suite.suite_name LIKE '%%flinging_test_bin.html%%' OR test_suite.suite_name = 'TestFlinging') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Canary_Canary_Release",
+        "title": "Chrome SDK v2 Flinging Test - Mac_Canary_Canary_Release"
+  },
+  "Chrome SDK v2 Flinging Test - Win_Canary_Canary_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_canary_canary_release' AND (test_suite.suite_name LIKE '%%flinging_test_bin.html%%' OR test_suite.suite_name = 'TestFlinging') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Canary_Canary_Release",
+        "title": "Chrome SDK v2 Flinging Test - Win_Canary_Canary_Release"
+  },
+  "Chrome SDK v2 Flinging Test - Mac_Beta_Staging_Dogfood ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+           "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_beta_staging_dogfood' AND (test_suite.suite_name LIKE '%%flinging_test_bin.html%%' OR test_suite.suite_name = 'TestFlinging') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Beta_Staging_Dogfood",
+        "title": "Chrome SDK v2 Flinging Test - Mac"
+    },
+  "Chrome SDK v2 Flinging Test - Win_Beta_Staging_Dogfood ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_beta_staging_dogfood' AND (test_suite.suite_name LIKE '%%flinging_test_bin.html%%' OR test_suite.suite_name = 'TestFlinging') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Beta_Staging_Dogfood",
+        "title": "Chrome SDK v2 Flinging Test - Win_Beta_Staging_Dogfood"
+  },
+  "Chrome SDK v2 Flinging Test - Mac_Canary_Dev_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_canary_dev_release' AND (test_suite.suite_name LIKE '%%flinging_test_bin.html%%' OR test_suite.suite_name = 'TestFlinging') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Canary_Dev_Release",
+        "title": "Chrome SDK v2 Flinging Test - Mac_Canary_Dev_Release"
+  },
+  "Chrome SDK v2 Flinging Test - Win_Canary_Dev_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_canary_dev_release' AND (test_suite.suite_name LIKE '%%flinging_test_bin.html%%' OR test_suite.suite_name = 'TestFlinging') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Canary_Dev_Release",
+        "title": "Chrome SDK v2 Flinging Test - Win_Canary_Dev_Release"
+  },
+  "Chrome SDK v2 Joining Test - Mac_Canary_Canary_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_canary_canary_release' AND (test_suite.suite_name LIKE '%%/join_test_bin.html%%' OR test_suite.suite_name = 'TestJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Canary_Canary_Release",
+        "title": "Chrome SDK v2 Join Test - Mac_Canary_Canary_Release"
+  },
+  "Chrome SDK v2 Joining Test - Win_Canary_Canary_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_canary_canary_release' AND (test_suite.suite_name LIKE '%%/join_test_bin.html%%' OR test_suite.suite_name = 'TestJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Canary_Canary_Release",
+        "title": "Chrome SDK v2 Join Test - Win_Canary_Canary_Release"
+  },
+  "Chrome SDK v2 Joining Test - Mac_Beta_Staging_Dogfood ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_beta_staging_dogfood' AND (test_suite.suite_name LIKE '%%/join_test_bin.html%%' OR test_suite.suite_name = 'TestJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Beta_Staging_Dogfood",
+        "title": "Chrome SDK v2 Joining Test - Mac_Beta_Staging_Dogfood"
+  },
+  "Chrome SDK v2 Joining Test - Win_Beta_Staging_Dogfood ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_beta_staging_dogfood' AND (test_suite.suite_name LIKE '%%/join_test_bin.html%%' OR test_suite.suite_name = 'TestJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Beta_Staging_Dogfood",
+        "title": "Chrome SDK v2 Join Test - Win_Beta_Staging_Dogfood"
+  },
+  "Chrome SDK v2 Joining Test - Mac_Canary_Dev_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_canary_dev_release' AND (test_suite.suite_name LIKE '%%/join_test_bin.html%%' OR test_suite.suite_name = 'TestJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Canary_Dev_Release",
+        "title": "Chrome SDK v2 Join Test - Mac_Canary_Dev_Release"
+  },
+  "Chrome SDK v2 Joining Test - Win_Canary_Dev_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_canary_dev_release' AND (test_suite.suite_name LIKE '%%/join_test_bin.html%%' OR test_suite.suite_name = 'TestJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Canary_Dev_Release",
+        "title": "Chrome SDK v2 Join Test - Win_Canary_Dev_Release"
+  },
+  "Chrome SDK v2 MDNS Test - Mac_Canary_Canary_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_canary_canary_release' AND (test_suite.suite_name LIKE '%%mdns_test_bin.html%%' OR test_suite.suite_name = 'TestmDNS') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Canary_Canary_Release",
+        "title": "Chrome SDK v2 MDNS Test - Mac_Canary_Canary_Release"
+  },
+  "Chrome SDK v2 MDNS Test - Win_Canary_Canary_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_canary_canary_release' AND (test_suite.suite_name LIKE '%%mdns_test_bin.html%%' OR test_suite.suite_name = 'TestmDNS') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Canary_Canary_Release",
+        "title": "Chrome SDK v2 MDNS Test - Win_Canary_Canary_Release"
+  },
+  "Chrome SDK v2 MDNS Test - Mac_Beta_Staging_Dogfood ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_beta_staging_dogfood' AND (test_suite.suite_name LIKE '%%mdns_test_bin.html%%' OR test_suite.suite_name = 'TestmDNS') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Beta_Staging_Dogfood",
+        "title": "Chrome SDK v2 MDNS Test - Mac_Beta_Staging_Dogfood"
+  },
+  "Chrome SDK v2 MDNS Test - Win_Beta_Staging_Dogfood ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_beta_staging_dogfood' AND (test_suite.suite_name LIKE '%%mdns_test_bin.html%%' OR test_suite.suite_name = 'TestmDNS') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Beta_Staging_Dogfood",
+        "title": "Chrome SDK v2 MDNS Test - Win_Beta_Staging_Dogfood"
+  },
+  "Chrome SDK v2 MDNS Test - Mac_Canary_Dev_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_canary_dev_release' AND (test_suite.suite_name LIKE '%%mdns_test_bin.html%%' OR test_suite.suite_name = 'TestmDNS') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Canary_Dev_Release",
+        "title": "Chrome SDK v2 MDNS Test - Mac_Canary_Dev_Release"
+  },
+  "Chrome SDK v2 MDNS Test - Win_Canary_Dev_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_canary_dev_release' AND (test_suite.suite_name LIKE '%%mdns_test_bin.html%%' OR test_suite.suite_name = 'TestmDNS') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Canary_Dev_Release",
+        "title": "Chrome SDK v2 MDNS Test - Win_Canary_Dev_Release"
+  },
+  "Chrome SDK v2 Media Joining Test - Mac_Canary_Canary_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_canary_canary_release' AND (test_suite.suite_name LIKE '%%media_join_test_bin.html%%' OR test_suite.suite_name = 'TestMediaJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Canary_Canary_Release",
+        "title": "Chrome SDK v2 Media Joining Test - Mac_Canary_Canary_Release"
+  },
+  "Chrome SDK v2 Media Joining Test - Win_Canary_Canary_Latest ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_canary_canary_release' AND (test_suite.suite_name LIKE '%%media_join_test_bin.html%%' OR test_suite.suite_name = 'TestMediaJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Canary_Canary_Release",
+        "title": "Chrome SDK v2 Media Joining Test - Win_Canary_Canary_Release"
+  },
+  "Chrome SDK v2 Media Joining Test - Mac_Beta_Staging_Dogfood ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_beta_staging_dogfood' AND (test_suite.suite_name LIKE '%%media_join_test_bin.html%%' OR test_suite.suite_name = 'TestMediaJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Beta_Staging_Dogfood",
+        "title": "Chrome SDK v2 Media Joining Test - Mac_Beta_Staging_Dogfood"
+  },
+  "Chrome SDK v2 Media Joining Test - Win_Beta_Staging_Dogfood ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_beta_staging_dogfood' AND (test_suite.suite_name LIKE '%%media_join_test_bin.html%%' OR test_suite.suite_name = 'TestMediaJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Beta_Staging_Dogfood",
+        "title": "Chrome SDK v2 Media Joining Test - Win_Beta_Staging_Dogfood"
+  },
+  "Chrome SDK v2 Media Joining Test - Mac_Canary_Dev_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_canary_dev_release' AND (test_suite.suite_name LIKE '%%media_join_test_bin.html%%' OR test_suite.suite_name = 'TestMediaJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Canary_Dev_Release",
+        "title": "Chrome SDK v2 Media Joining Test - Mac_Canary_Dev_Release"
+  },
+  "Chrome SDK v2 Media Joining Test - Win_Canary_Dev_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_canary_dev_release' AND (test_suite.suite_name LIKE '%%media_join_test_bin.html%%' OR test_suite.suite_name = 'TestMediaJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Canary_Dev_Release",
+        "title": "Chrome SDK v2 Media Joining Test - Win_Canary_Dev_Release"
+  },
+  "Chrome SDK v2 Auto Joining Test - Mac_Canary_Canary_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_canary_canary_release' AND (test_suite.suite_name LIKE '%%/auto_join_test_bin.html%%' OR test_suite.suite_name = 'TestAutoJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Canary_Canary_Release",
+        "title": "Chrome SDK v2 Auto Join Test - Mac_Canary_Canary_Release"
+  },
+  "Chrome SDK v2 Auto Joining Test - Win_Canary_Canary_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_canary_canary_release' AND (test_suite.suite_name LIKE '%%/auto_join_test_bin.html%%' OR test_suite.suite_name = 'TestAutoJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Canary_Canary_Release",
+        "title": "Chrome SDK v2 Auto Join Test - Win_Canary_Canary_Release"
+  },
+  "Chrome SDK v2 Auto Joining Test - Mac_Beta_Staging_Dogfood ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_beta_staging_dogfood' AND (test_suite.suite_name LIKE '%%/auto_join_test_bin.html%%' OR test_suite.suite_name = 'TestAutoJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Beta_Staging_Dogfood",
+        "title": "Chrome SDK v2 Auto Joining Test - Mac_Beta_Staging_Dogfood"
+  },
+  "Chrome SDK v2 Auto Joining Test - Win_Beta_Staging_Dogfood ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_beta_staging_dogfood' AND (test_suite.suite_name LIKE '%%/auto_join_test_bin.html%%' OR test_suite.suite_name = 'TestAutoJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Beta_Staging_Dogfood",
+        "title": "Chrome SDK v2 Auto Joining Test - Win_Beta_Staging_Dogfood"
+  },
+  "Chrome SDK v2 Auto Joining Test - Mac_Canary_Dev_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_mac_canary_dev_release' AND (test_suite.suite_name LIKE '%%/auto_join_test_bin.html%%' OR test_suite.suite_name = 'TestAutoJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Mac_Canary_Dev_Release",
+        "title": "Chrome SDK v2 Auto Join Test - Mac_Canary_Dev_Release"
+  },
+  "Chrome SDK v2 Auto Joining Test - Win_Canary_Dev_Release ": {
+        "configuration": {
+            "hAxis": {
+                "slantedText": true
+            }
+        },
+        "device_type": "eureka",
+        "sql_data": {
+            "column_types": [
+                "string",
+                "number",
+                "number",
+                "number"
+            ],
+            "columns_to_plot": [
+                0,
+                1,
+                2,
+                3
+            ],
+            "headers": {
+                "Time": 0,
+                "Tests Run": 1,
+                "Failures": 2,
+                "Errors": 3
+            },
+            "query": "SELECT datetime(test_run.start_timestamp, 'localtime'), test_suite.test_count, test_suite.failure_count, test_suite.error_count FROM test_run, test_suite WHERE test_run.run_id = test_suite.run_id AND test_run.builder_name = 'chrome_sdk_end_to_end_v2_win_canary_dev_release' AND (test_suite.suite_name LIKE '%%/auto_join_test_bin.html%%' OR test_suite.suite_name = 'TestAutoJoin') AND test_run.test_name = 'CastChromeEndToEndTestCase' AND datetime(test_run.start_timestamp) >= datetime('now', '-30 days') ORDER BY test_run.start_timestamp;"
+        },
+        "table_description": [
+            ["Time", "string"],
+            ["Tests Run", "number"],
+            ["Failures", "number"],
+            ["Errors", "number"]
+        ],
+        "test_category": "sdk_Win_Canary_Dev_Release",
+        "title": "Chrome SDK v2 Auto Join Test - Win_Canary_Dev_Release"
+  }
+}
diff --git a/lab_system/master/config/kir/device_type/dummy.cfg b/lab_system/master/config/kir/device_type/dummy.cfg
new file mode 100755
index 0000000..90c2b48
--- /dev/null
+++ b/lab_system/master/config/kir/device_type/dummy.cfg
@@ -0,0 +1,29 @@
+# Config file for dummy device type.
+#
+# To activate a change in a configuration value, run:
+#
+# buildbot reconfig
+#
+# That will force the running buildbot instance to reconfigure
+# and use the new configuration values.
+#
+# It is generally safe to change configuration values,
+# but changing configuration names is not recommended as it will result
+# in errors.
+#
+# Every device_type to test requires a device_type config file.
+#
+# PROPERTIES
+
+
+# Product name of dummy device.
+# PRODUCT_NAME value is passed to the GET_LATEST_BUILD_SCRIPT via the
+# --product flag.
+
+PRODUCT_NAME=dummy
+
+# Name to be used for <type_template> tests.
+# The value must correspond with the device types defined in the database
+# by the device allocator (galloc).
+
+DEVICE_TYPE=dummy
\ No newline at end of file
diff --git a/lab_system/master/config/kir/device_type/eureka.cfg b/lab_system/master/config/kir/device_type/eureka.cfg
new file mode 100644
index 0000000..60c066d
--- /dev/null
+++ b/lab_system/master/config/kir/device_type/eureka.cfg
@@ -0,0 +1,14 @@
+# Config file for eureka device type
+#
+# Do not modify the property names
+#
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# name to be used for eureka tests, usually eureka
+DEVICE_TYPE=eureka
+
+# ota zip file format is: PRODUCT_NAME-OTA_NAME-BUILD.zip
+PRODUCT_NAME=anchovy
diff --git a/lab_system/master/config/kir/groups/acer_i5_2450m_intelhd3000_win7.cfg b/lab_system/master/config/kir/groups/acer_i5_2450m_intelhd3000_win7.cfg
new file mode 100644
index 0000000..ee5a88d
--- /dev/null
+++ b/lab_system/master/config/kir/groups/acer_i5_2450m_intelhd3000_win7.cfg
@@ -0,0 +1,25 @@
+# Config file for acer_i5_2450m_intelhd3000_win7 slave
+
+# PROPERTIES
+
+SPECIFIC_DEVICE_IP=172.31.25.7
+SELF_IP=172.31.25.2
+
+HOME_DIR=C:/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver.exe
+CHROME_ZIP=chrome-win32.zip
+CHROME_PATH=chrome-win32
+PLATFORM=win
+
+CHROME_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/latest_chrome/acer_i5_2450m_intelhd3000_win7
+CHROME_VERSION=canary
+EXTENSION_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/extension/acer_i5_2450m_intelhd3000_win7
+EXTENSION_BUILD=canary
+
+MASTER_HOME_DIR=/usr/local/google/home/repositories
+CHROME_PERF_DASHBOARD=https://chromeperf.appspot.com/add_point
+
+# TODO (cliffordcheng): To be added
+#DEVICE_TYPE=win
+#DEVICE_GROUP=acer_i5_2450m_intelhd3000_win7
diff --git a/lab_system/master/config/kir/groups/acer_i5_3337u_intelhd4000_win8.cfg b/lab_system/master/config/kir/groups/acer_i5_3337u_intelhd4000_win8.cfg
new file mode 100755
index 0000000..3c20b96
--- /dev/null
+++ b/lab_system/master/config/kir/groups/acer_i5_3337u_intelhd4000_win8.cfg
@@ -0,0 +1,32 @@
+# Config file for acer_i5_3337u_intelhd4000_win8 slave
+
+# PROPERTIES
+
+SPECIFIC_DEVICE_IP=172.31.25.7
+SELF_IP=172.31.25.20
+
+HOME_DIR=C:/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver.exe
+CHROME_ZIP=chrome-win32.zip
+CHROME_PATH=chrome-win32
+PLATFORM=win
+
+CHROME_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/latest_chrome/acer_i5_3337u_intelhd4000_win8
+CHROME_VERSION=canary
+EXTENSION_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/extension/acer_i5_3337u_intelhd4000_win8
+EXTENSION_BUILD=staging
+
+MASTER_HOME_DIR=/usr/local/google/home/repositories
+CHROME_PERF_DASHBOARD=https://chromeperf.appspot.com/add_point
+
+SENDER_DEVICE_TYPE=mac
+SENDER_DEVICE_GROUP=video_recorder_set_0
+SENDER_DEVICE_NUMBER=1
+
+PROCESSOR_MACHINE_IP=172.31.25.28
+PROCESSOR_DEVICE_TYPE=linux
+PROCESSOR_DEVICE_GROUP=video_processing_machine_0
+PROCESSOR_DEVICE_NUMBER=1
+PROCESSOR_RESULTS_DIR=/home/gtvchrome/results
+PROCESSOR_TEST_DIR=/home/gtvchrome/repository/eureka/test
diff --git a/lab_system/master/config/kir/groups/asus_i3_2328m_intelhd3000_win8.cfg b/lab_system/master/config/kir/groups/asus_i3_2328m_intelhd3000_win8.cfg
new file mode 100644
index 0000000..386ba09
--- /dev/null
+++ b/lab_system/master/config/kir/groups/asus_i3_2328m_intelhd3000_win8.cfg
@@ -0,0 +1,31 @@
+# Config file for asus_i3_2328m_intelhd3000_win8 slave
+
+# PROPERTIES
+
+SELF_IP=172.31.25.2
+
+HOME_DIR=C:/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver.exe
+CHROME_ZIP=chrome-win32.zip
+CHROME_PATH=chrome-win
+PLATFORM=win
+
+CHROME_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/latest_chrome/asus_i3_2328m_intelhd3000_win8
+CHROME_VERSION=canary
+EXTENSION_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/extension/asus_i3_2328m_intelhd3000_win8
+EXTENSION_BUILD=staging
+
+MASTER_HOME_DIR=/usr/local/google/home/repositories
+CHROME_PERF_DASHBOARD=https://chromeperf.appspot.com/add_point
+
+SENDER_DEVICE_TYPE=mac
+SENDER_DEVICE_GROUP=video_recorder_set_0
+SENDER_DEVICE_NUMBER=1
+
+PROCESSOR_MACHINE_IP=172.31.25.28
+PROCESSOR_DEVICE_TYPE=linux
+PROCESSOR_DEVICE_GROUP=video_processing_machine_0
+PROCESSOR_DEVICE_NUMBER=1
+PROCESSOR_RESULTS_DIR=/home/gtvchrome/results
+PROCESSOR_TEST_DIR=/home/gtvchrome/repository/eureka/test
diff --git a/lab_system/master/config/kir/groups/bling_cast.cfg b/lab_system/master/config/kir/groups/bling_cast.cfg
new file mode 100644
index 0000000..90fd6d1
--- /dev/null
+++ b/lab_system/master/config/kir/groups/bling_cast.cfg
@@ -0,0 +1,31 @@
+# Configuration values for the bling Web SDK e2e test.
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.bling.end_to_end_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastBlingEndToEndTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.cast.bling.end_to_end_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CastBlingEndToEndTestSequence
+
+# The mac device's username
+MAC_USERNAME=gtvchrome
+
+# The type of the macbook to be used
+MAC_DEVICE_TYPE=mac
+
+# The group of the macbook to be used
+MAC_DEVICE_GROUP=bling_cast
+
+# The mac device's home dir
+MAC_HOME_DIR=/Users/gtvchrome
+
+# The type of the iOS to be used, using 32 bit iOS device
+IOS_DEVICE_TYPE=iphone
+
+# The group of the iOS device to be used
+IOS_DEVICE_GROUP=bling_cast
diff --git a/lab_system/master/config/kir/groups/chrome_sdk_end_to_end_v2.cfg b/lab_system/master/config/kir/groups/chrome_sdk_end_to_end_v2.cfg
new file mode 100755
index 0000000..faf776b
--- /dev/null
+++ b/lab_system/master/config/kir/groups/chrome_sdk_end_to_end_v2.cfg
@@ -0,0 +1,31 @@
+# Config file for chrome_sdk_end_to_end_v2 group
+
+# PROPERTIES
+REMOTE_TEST_FILES_FOLDER=/var/www/http
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.chrome.end_to_end_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastChromeEndToEndTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+# Location of test binary
+TEST_BINARY_FILE=gs://test_logs/test_binaries/test_bin.zip
+
+# Location of changelist file
+EXTENSION_CHANGELIST_FILE=gs://test_logs/test_binaries/changelist.txt
+
+# Location of dev extension
+DEV_EXTENSION_FILE=gs://test_logs/test_binaries/extension_dev.zip
+
+# Account information used to download internal cast extension
+ACCOUNT_FILE=/usr/local/google/home/repository/credentials/account.txt
+
+# Public keys for different cast extensions
+EXTENSION_PUBLIC_KEYS_FILE=/usr/local/google/home/repository/credentials/public_keys.json
diff --git a/lab_system/master/config/kir/groups/hp_core2duo_p8600_gma4500_win7.cfg b/lab_system/master/config/kir/groups/hp_core2duo_p8600_gma4500_win7.cfg
new file mode 100644
index 0000000..fc48317
--- /dev/null
+++ b/lab_system/master/config/kir/groups/hp_core2duo_p8600_gma4500_win7.cfg
@@ -0,0 +1,33 @@
+# Config file for hp_core2duo_p8600_gma4500_win7 slave
+
+# PROPERTIES
+
+SPECIFIC_DEVICE_IP=172.31.25.1
+SELF_IP=172.31.25.17
+
+HOME_DIR=C:/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver.exe
+CHROME_ZIP=chrome-win32.zip
+CHROME_PATH=chrome-win32
+PLATFORM=win
+
+CHROME_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/latest_chrome/hp_core2duo_p8600_gma4500_win7
+CHROME_VERSION=canary
+EXTENSION_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/extension/hp_core2duo_p8600_gma4500_win7
+EXTENSION_BUILD=staging
+
+MASTER_HOME_DIR=/usr/local/google/home/repositories
+CHROME_PERF_DASHBOARD=https://chromeperf.appspot.com/add_point
+
+SENDER_DEVICE_TYPE=mac
+SENDER_DEVICE_GROUP=video_recorder_set_1
+SENDER_DEVICE_NUMBER=1
+
+PROCESSOR_MACHINE_IP=172.31.25.28
+PROCESSOR_DEVICE_TYPE=linux
+PROCESSOR_DEVICE_GROUP=video_processing_machine_1
+PROCESSOR_DEVICE_NUMBER=1
+PROCESSOR_RESULTS_DIR=/home/gtvchrome/results
+PROCESSOR_TEST_DIR=/home/gtvchrome/repository/eureka/test
+
diff --git a/lab_system/master/config/kir/groups/hp_core_i5_3210m_intelhd4000_win8_mtv.cfg b/lab_system/master/config/kir/groups/hp_core_i5_3210m_intelhd4000_win8_mtv.cfg
new file mode 100644
index 0000000..834ac55
--- /dev/null
+++ b/lab_system/master/config/kir/groups/hp_core_i5_3210m_intelhd4000_win8_mtv.cfg
@@ -0,0 +1,30 @@
+# Config file for hp_core_i5 in MTV lab.
+
+# PROPERTIES
+SELF_IP=172.17.23.78
+HOME_DIR=C:/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver.exe
+PLATFORM=win
+
+# Mirroring test chrome build
+CHROME_VERSION=beta
+
+# Mirroring test extension flavor
+EXTENSION_BUILD=beta
+
+# Recorder machine
+SENDER_DEVICE_TYPE=mac
+SENDER_DEVICE_GROUP=video_recorder_mtv
+SENDER_DEVICE_NUMBER=1
+
+
+PROCESSOR_MACHINE_IP=172.31.25.28
+PROCESSOR_DEVICE_TYPE=linux
+PROCESSOR_DEVICE_GROUP=video_processing_machine_0
+PROCESSOR_DEVICE_NUMBER=1
+PROCESSOR_RESULTS_DIR=/home/gtvchrome/results
+PROCESSOR_TEST_DIR=/home/gtvchrome/repository/eureka/test
+
+
+
diff --git a/lab_system/master/config/kir/groups/macbookair_i5_2557m_intelhd3000.cfg b/lab_system/master/config/kir/groups/macbookair_i5_2557m_intelhd3000.cfg
new file mode 100644
index 0000000..4751c2d
--- /dev/null
+++ b/lab_system/master/config/kir/groups/macbookair_i5_2557m_intelhd3000.cfg
@@ -0,0 +1,27 @@
+# Config file for macbookair_i5_2557m_intelhd3000 slave
+
+# PROPERTIES
+
+SPECIFIC_DEVICE_IP=172.31.25.3
+SELF_IP=172.31.25.29
+
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver
+CHROME_ZIP=chrome-mac.zip
+CHROME_PATH=chrome-mac
+PLATFORM=mac
+
+CHROME_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/latest_chrome/macbookair_i5_2557m_intelhd3000
+CHROME_VERSION=canary
+EXTENSION_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/extension/macbookair_i5_2557m_intelhd3000
+EXTENSION_BUILD=canary
+
+MASTER_HOME_DIR=/usr/local/google/home/repositories
+CHROME_PERF_DASHBOARD=https://chromeperf.appspot.com/add_point
+
+SENDER_DEVICE_TYPE=mac
+SENDER_DEVICE_GROUP=video_recorder_set_0
+SENDER_DEVICE_NUMBER=1
+
+PROCESSOR_MACHINE_IP=172.31.25.11
diff --git a/lab_system/master/config/kir/groups/macbookair_i5_4250u_intelhd5000.cfg b/lab_system/master/config/kir/groups/macbookair_i5_4250u_intelhd5000.cfg
new file mode 100755
index 0000000..d6da498
--- /dev/null
+++ b/lab_system/master/config/kir/groups/macbookair_i5_4250u_intelhd5000.cfg
@@ -0,0 +1,33 @@
+# Config file for macbookair_i5_4250u_intelhd5000 slave
+
+# PROPERTIES
+
+SPECIFIC_DEVICE_IP=172.31.25.19
+SELF_IP=172.31.25.27
+
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver
+CHROME_ZIP=chrome-mac.zip
+CHROME_PATH=chrome-mac
+PLATFORM=mac
+
+CHROME_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/latest_chrome/macbookair_i5_4250u_intelhd5000
+CHROME_VERSION=canary
+EXTENSION_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/extension/macbookair_i5_4250u_intelhd5000
+EXTENSION_BUILD=staging
+
+MASTER_HOME_DIR=/usr/local/google/home/repositories
+CHROME_PERF_DASHBOARD=https://chromeperf.appspot.com/add_point
+
+SENDER_DEVICE_TYPE=mac
+SENDER_DEVICE_GROUP=video_recorder_set_1
+SENDER_DEVICE_NUMBER=1
+
+PROCESSOR_MACHINE_IP=172.31.25.28
+PROCESSOR_DEVICE_TYPE=linux
+PROCESSOR_DEVICE_GROUP=video_processing_machine_1
+PROCESSOR_DEVICE_NUMBER=1
+PROCESSOR_RESULTS_DIR=/home/gtvchrome/results
+PROCESSOR_TEST_DIR=/home/gtvchrome/repository/eureka/test
+
diff --git a/lab_system/master/config/kir/groups/macbookair_i5_4250u_intelhd5000_mtv.cfg b/lab_system/master/config/kir/groups/macbookair_i5_4250u_intelhd5000_mtv.cfg
new file mode 100644
index 0000000..d7321cc
--- /dev/null
+++ b/lab_system/master/config/kir/groups/macbookair_i5_4250u_intelhd5000_mtv.cfg
@@ -0,0 +1,27 @@
+onfig file for macbook air in MTV lab.
+
+# PROPERTIES
+SELF_IP=172.17.23.86
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver
+PLATFORM=mac
+
+# Mirroring test chrome build
+CHROME_VERSION=beta
+
+# Mirroring test extension flavor
+EXTENSION_BUILD=beta
+
+# Recorder machine
+SENDER_DEVICE_TYPE=mac
+SENDER_DEVICE_GROUP=video_recorder_mtv
+SENDER_DEVICE_NUMBER=1
+
+
+PROCESSOR_MACHINE_IP=172.31.25.28
+PROCESSOR_DEVICE_TYPE=linux
+PROCESSOR_DEVICE_GROUP=video_processing_machine_0
+PROCESSOR_DEVICE_NUMBER=1
+PROCESSOR_RESULTS_DIR=/home/gtvchrome/results
+PROCESSOR_TEST_DIR=/home/gtvchrome/repository/eureka/test
diff --git a/lab_system/master/config/kir/groups/macbookpro_i7_2675qm_radeon6750m.cfg b/lab_system/master/config/kir/groups/macbookpro_i7_2675qm_radeon6750m.cfg
new file mode 100755
index 0000000..ad0aeea
--- /dev/null
+++ b/lab_system/master/config/kir/groups/macbookpro_i7_2675qm_radeon6750m.cfg
@@ -0,0 +1,33 @@
+# Config file for macbookpro_i7_2675qm_radeon6750m slave
+
+# PROPERTIES
+
+SPECIFIC_DEVICE_IP=172.31.25.8
+SELF_IP=172.31.25.22
+
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver
+CHROME_ZIP=chrome-mac.zip
+CHROME_PATH=chrome-mac
+PLATFORM=mac
+
+CHROME_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/latest_chrome/macbookpro_i7_2675qm_radeon6750m
+CHROME_VERSION=canary
+EXTENSION_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/extension/macbookpro_i7_2675qm_radeon6750m
+EXTENSION_BUILD=staging
+
+MASTER_HOME_DIR=/usr/local/google/home/repositories
+CHROME_PERF_DASHBOARD=https://chromeperf.appspot.com/add_point
+
+SENDER_DEVICE_TYPE=mac
+SENDER_DEVICE_GROUP=video_recorder_set_1
+SENDER_DEVICE_NUMBER=1
+
+PROCESSOR_MACHINE_IP=172.31.25.28
+PROCESSOR_DEVICE_TYPE=linux
+PROCESSOR_DEVICE_GROUP=video_processing_machine_1
+PROCESSOR_DEVICE_NUMBER=1
+PROCESSOR_RESULTS_DIR=/home/gtvchrome/results
+PROCESSOR_TEST_DIR=/home/gtvchrome/repository/eureka/test
+
diff --git a/lab_system/master/config/kir/groups/samsung_i7_3630qm_radeon7730m_win8.cfg b/lab_system/master/config/kir/groups/samsung_i7_3630qm_radeon7730m_win8.cfg
new file mode 100644
index 0000000..29dca4b
--- /dev/null
+++ b/lab_system/master/config/kir/groups/samsung_i7_3630qm_radeon7730m_win8.cfg
@@ -0,0 +1,30 @@
+# Config file for samsung_i7_3630qm_radeon7730m_win8 slave
+
+# PROPERTIES
+
+SPECIFIC_DEVICE_IP=172.31.25.10
+SELF_IP=172.31.25.15
+
+HOME_DIR=C:/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver.exe
+CHROME_ZIP=chrome-win32.zip
+CHROME_PATH=chrome-win32
+PLATFORM=win
+
+CHROME_VERSION=canary
+EXTENSION_BUILD=staging
+
+MASTER_HOME_DIR=/usr/local/google/home/repositories
+CHROME_PERF_DASHBOARD=https://chromeperf.appspot.com/add_point
+
+SENDER_DEVICE_TYPE=mac
+SENDER_DEVICE_GROUP=video_recorder_set_0
+SENDER_DEVICE_NUMBER=1
+
+PROCESSOR_MACHINE_IP=172.31.25.28
+PROCESSOR_DEVICE_TYPE=linux
+PROCESSOR_DEVICE_GROUP=video_processing_machine_0
+PROCESSOR_DEVICE_NUMBER=1
+PROCESSOR_RESULTS_DIR=/home/gtvchrome/results
+PROCESSOR_TEST_DIR=/home/gtvchrome/repository/eureka/test
diff --git a/lab_system/master/config/kir/groups/trial_machine_win.cfg b/lab_system/master/config/kir/groups/trial_machine_win.cfg
new file mode 100755
index 0000000..69b3077
--- /dev/null
+++ b/lab_system/master/config/kir/groups/trial_machine_win.cfg
@@ -0,0 +1,32 @@
+# Config file for trial_machine_win  slave
+
+# PROPERTIES
+
+SPECIFIC_DEVICE_IP=172.31.25.7
+SELF_IP=172.31.25.20
+
+HOME_DIR=C:/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver.exe
+CHROME_ZIP=chrome-win32.zip
+CHROME_PATH=chrome-win32
+PLATFORM=win
+
+CHROME_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/latest_chrome/trial_machine_win
+CHROME_VERSION=canary
+EXTENSION_FOLDER=/usr/local/google/home/repositories/fraggle_rock/config/tests/chrome_mirroring/extension/trial_machine_win
+EXTENSION_BUILD=staging
+
+MASTER_HOME_DIR=/usr/local/google/home/repositories
+CHROME_PERF_DASHBOARD=https://chromeperf.appspot.com/add_point
+
+SENDER_DEVICE_TYPE=mac
+SENDER_DEVICE_GROUP=video_recorder_set_0
+SENDER_DEVICE_NUMBER=1
+
+PROCESSOR_MACHINE_IP=172.31.25.28
+PROCESSOR_DEVICE_TYPE=linux
+PROCESSOR_DEVICE_GROUP=video_processing_machine_0
+PROCESSOR_DEVICE_NUMBER=1
+PROCESSOR_RESULTS_DIR=/home/gtvchrome/results
+PROCESSOR_TEST_DIR=/home/gtvchrome/repository/eureka/test
diff --git a/lab_system/master/config/kir/site/site.cfg b/lab_system/master/config/kir/site/site.cfg
new file mode 100755
index 0000000..dce4e9f
--- /dev/null
+++ b/lab_system/master/config/kir/site/site.cfg
@@ -0,0 +1,299 @@
+# Config file for site wide values.
+#
+# These values are expected to change on a site by site basis. If the system
+# is set up in a new location, these values would need to be changed to work
+# at that location.
+#
+# To activate a change in a configuration value, run:
+#
+# buildbot reconfig
+#
+# That will force the running buildbot instance to reconfigure
+# and use the new configuration values.
+#
+# It is generally safe to change configuration values, but changing or
+# deleting configuration names is not recommended as it will result
+# in errors.
+#
+# PROPERTIES
+
+# EUREKA_TEST_DIRECTORY is the same as INTERNAL_REPOSITYORY_PATH
+EUREKA_TEST_DIRECTORY=/usr/local/google/home/repository/eureka
+
+# Enables functionality that is only available within Google if set to 1.
+# If running externally, set this to 0 to ensure that all Google specific
+# functionality is not run.
+
+G_INTERNAL=0
+
+# CTS reporting root directory. It should be a place accessible from web to
+# view results.
+
+CTS_REPORTING_DIRECTORY=/path/to/cts/reporting/dir
+
+# Path to logs storage dir when tests run.
+
+LOCAL_LOGS_DIRECTORY=/usr/local/google/home/logs/logs
+
+# Bug search tool.
+
+BUG_SEARCH_COMMAND=/path/to/tool --param=
+
+# The script defined in GET_LATEST_BUILD_SCRIPT property identifies the path and
+# build number of the latest Google TV build to use for testing.
+#
+# Before each test is launched, the script is executed. The script is expected
+# to output the path to the latest build when --output_format is set to "path".
+# It is expected to output the number of the latest build
+# when --output_format is set to "number".
+#
+# If --build_number is provided, the script is expected to output the path to
+# the build zip file for that build number.
+#
+# It is up to the user to implement the script. It can be a simple bash script
+# or a more involved binary. The only requirement is that the script adheres to
+# the command line signature detailed below.
+#
+# An implementation of the script is free to use or ignore the values of the
+# command line flags passed to the script. However, it must accept the flags.
+# If an implementation of the script does not accept these flags,
+# all tests will fail at the GET_BUILD step.
+#
+# For convenience, a template python script exists in
+# lab_system/get_latest_build.py
+# That template can be copied and extended for a new site installation.
+#
+# The GET_LATEST_BUILD_SCRIPT is passed the following parameters. Again,
+# implementations can use or ignore these values internally, but they must allow
+# them.
+#
+# --build_number
+#
+# This is the number of a build. When provided, the script is expected to output
+# the path to the given build number.
+#
+# --output_mode
+#
+# This defines how the output is formatted. There are two values used: "path"
+# and "number".
+#
+# In "path" mode, the full path to the latest build zip file is output by
+# the script.
+# In "number" mode, the latest build number is output by the script.
+#
+# --branch
+#
+# This is the branch of the build. This corresponds to the BRANCH_NAME
+# value in the config/branch/<branch>.cfg file.
+#
+# --product
+#
+# This is the code name of the Google TV product.
+# Different implementations of Google TV (eg. buddy box vs. a TV)
+# have identifying code names.
+# This parameter specifies which implementation is to be tested.
+#
+# This parameter will be set to the PRODUCT_NAME specified in
+# config/device_type/<device_type>.cfg
+#
+# --build_output_type
+#
+# This is the type of the output produced by the build process.
+# The "ota" build is the build zip file that is installed on a Google TV device.
+# The "symbols" build is generated by the build process to enable symbolization
+# of stack traces from the native code level.
+# The "tests" build zip contains the binaries for unit tests
+# and native system tests.
+#
+# The "ota" build is most commonly used; tests will not run if the
+# GET_LATEST_BUILD_SCRIPT does not work with the "ota" flag. "symbols" is
+# optional; if a symbolized build zip is not available, the only impact is
+# that the parsed logs from tests will not have symbolized stack traces.
+#
+# "tests" is required if Google TV unit tests are being run.
+#
+# --flavor
+#
+# The values passed to --flavor are "eng" or "tests".
+# "eng" specifies the eng build is to be used.
+#
+# This is a Google TV build that is built with "eng" permissions.
+# "tests" is for Google TV builds that are output when building the "tests"
+# build flavor. For more information on build flavors, see the Google TV build
+# documentation.
+#
+# "tests" is required if Google TV unit tests are being run.
+#
+# The GET_LATEST_BUILD_SCRIPT script must be executable by the user
+# running the buildbot installation.
+#
+# A very basic test to ensure that the script adheres to the command line
+# signature is to run it with the flags, eg.
+#
+# /path/to/script --branch=foo --product=bar --build_output_type=ota --flavor=eng
+# --output_mode=path
+
+GET_LATEST_BUILD_SCRIPT=/usr/local/google/home/repository/eureka/continuous-tests/build/get_latest_build.py
+
+# Root location where tests logs are stored
+RESULTS=gs://test_logs
+
+# Base URL to Google Storage console
+GS_CONSOLE_BASE_URL=https://cloud.google.com/console#/storage/
+
+# Waterfall server port.
+
+URL_PORT=8010
+
+# CSV root dir.
+
+CSV_ROOT=/var/www/logs
+
+# Port used for communication between master and slaves:
+
+MASTER_SLAVE_PORT_NUMBER=8014
+
+# Parsed root dir. It should be accessible from web.
+
+PARSED_ROOT=/path/to/parsed/root/dir
+
+# Dashboard server port.
+
+SERVER_PORT=8020
+
+# Path to file for storing event histories.
+
+HISTORY_PROVIDER_FILE_PATH=/path/to/event_history.sqlite
+
+# Database type.
+# The currently supported backend databases are defined in db/database_types.py.
+# The default database type is Sqlite.  To use a Sqlite database:
+# Set DB_TYPE=sqlite and set DB_PATH=/path, where path is the location where
+# the Sqlite database will be stored.
+#
+# To use a PostgreSQL database:
+# Set DB_TYPE=postgresql and set DB_PATH=/path, where path is the location where
+# the PostgreSQL database configuration file exists.  An example PostgreSQL
+# configuration file is at db/db.config.
+#
+# NOTE: If using postgresql, the third party psycopg2 library must be installed
+# in your python environment.  If not, ImportErrors will occur when using any of
+# the tools that interface with the database.
+
+DB_TYPE=sqlite
+
+# Master hostname.
+
+MASTER_HOSTNAME=cloudview-test.cbf.corp.google.com
+
+# Password for communication between buildbot master and slaves.  This password
+# should not be set to an actual user password.  It purely governs the
+# communication between the master and slaves.
+
+MASTER_SLAVE_PASSWORD=buildbotmasterslavepassword
+
+# Dashboard server entry point, could be either an IP or a hostname.
+# Defines the host name that the web server will listen on. For example,
+# the results dashboard will be available at SERVER_HOSTNAME:8080/dashboard.
+
+SERVER_HOSTNAME=0.0.0.0
+
+# Cts package location.
+# Format is <CTS_ROOT>/<CTS_DIRECTORY>/<CTS_PACKAGE_LOCATION>
+# <CTS_DIRECTORY> is in config/device_type/<device_type>.cfg.
+# It usually will be repository/testcases.
+
+CTS_PACKAGE_LOCATION=repository/testcases
+
+# Location of any useful system monitoring files. System monitoring files can
+# be stored here and will be accessible via the http server /monitor/ route.
+
+MONITOR_FILE_PATH=/path/to/dir
+
+# Emails will be sent to any email address listed here. Additional notifier
+# names must have incrementing numbers:
+# EMAIL_NOTIFIER_1, EMAIL_NOTIFIER_2, EMAIL_NOTIFIER_N
+
+EMAIL_NOTIFIER_1=leilei@google.com
+
+# Web SDK test notification mail notifiers:
+WEB_SDK_EMAIL_NOTIFIERS=cloudview-build+sdk-automation@google.com
+
+# Name for the continuous tests system. The value will appear in the waterfall
+# page.
+
+NAME=Chromecast continuous tests system
+
+# Web accessible location to upload logcats.
+
+WEB_LOG_DIR=/path/to/upload/root/dir
+
+# cts media path
+
+CTS_MEDIA_PATH=/path/to/android-cts-media
+
+# Path to the base output directory in which all test data is written:
+
+BASE_OUTPUT_DIRECTORY=/tmp
+
+# Parsed http endpoint.
+
+PARSED_LINK=http://path.to.parsed/dir
+
+# Email sender.
+
+EMAIL_SENDER=cloudview_test@google.com
+
+# HTTP endpoint for reaching WEB_LOG_DIR.  URLs will be constructed as:
+# WEB_HREF/WEB_LOG_DIR/..
+
+WEB_HREF=http://path
+
+# Directory where cts is installed. Format is <CTS_ROOT>/<CTS_DIRECTORY>
+# <CTS_DIRECTORY> is in config/device_type/<device_type>.cfg.
+
+CTS_ROOT=/path/to/cts/installation
+
+# Location of script to find apps that will be installed on the new build.
+
+UNBUNDLED_APK_IDENTIFIER=/path/to/script.sh
+
+# Are you going to run in production? [yes for prod/no for staging]
+PROD=0
+
+# System specific command to make a directory.
+
+MKDIR_COMMAND=mkdir -p
+
+# To copy files from repository, usually will be cp, but it can be different
+# if using a specific system.
+
+CP_COMMAND=cp
+
+# For a PostgreSQL database backend (ie. DB_TYPE=postgresql), provide the path
+# to the PostgreSQL database configuration file (see notes for DB_TYPE above).
+#
+# For a Sqlite database backend (ie. DB_TYPE=sqlite), provide the path to the
+# Sqlite database file:
+
+DB_PATH=/usr/local/google/home/repository/fraggle_rock/database/sqlite.db
+
+# Chrome cast server and directory
+HTTP_SERVER_IP=172.17.32.130
+
+# User name of HTTPS server.
+HTTP_SERVER_USERNAME=gtvchrome
+
+# Base URL of log server
+LOGS_SERVER=https://console.developers.google.com/project/apps~cast-in-chrome/storage/test_logs
+
+# Location where build status JSON is expected:
+BUILD_STATUS_DIRECTORY_PATH=/tmp/build/status/
+
+BUILD_INSTALLER_PATH=/usr/local/google/home/repository/eureka/test/ota/ota_update.py
+
+# URL endpoint for the chromeperf dashboard for adding data. Your host must be
+# whitelisted to use the default value. A dev endpoint is available at:
+#   https://chrome-perf.googleplex.com/add_point
+# This endpoint will accept data and return a 200 if it is correctly formatted.
+CHROME_PERF_UPLOAD_URL=https://chromeperf.appspot.com/add_point
diff --git a/lab_system/master/config/kir/steps/default.cfg b/lab_system/master/config/kir/steps/default.cfg
new file mode 100644
index 0000000..4e87f49
--- /dev/null
+++ b/lab_system/master/config/kir/steps/default.cfg
@@ -0,0 +1,94 @@
+[
+  {
+    "step_description": "Sets up testrun specific properties",
+    "step_identifier": "properties",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.setup.properties",
+        "class_name": "PropertiesSetterStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Allocates devices for the testrun",
+    "step_identifier": "device_allocation",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.allocator",
+        "class_name": "DeviceAllocatorStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Fetches and installs builds on devices",
+    "step_identifier": "build_handling",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.build.fetcher",
+        "class_name": "BuildFetcherStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.build.installer_eurlab",
+        "class_name": "BuildInstallerStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Sets up slave and devices for the testrun.  Currently a placeholder with no-ops.",
+    "step_identifier": "testrun_setup",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs_eurlab",
+        "class_name": "StartLoggingStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Executes the tests using the eurtest runner.",
+    "step_identifier": "test_running",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.run.test_runner",
+        "class_name": "TestRunnerStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Replaces logs_copy with a no-op",
+    "step_identifier": "logs_copy",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.common.noop",
+        "class_name": "NoopStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Processes the output of the test",
+    "step_identifier": "output_processing",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs_eurlab",
+        "class_name": "StopLoggingStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs",
+        "class_name": "CopyLogsToRepositoryStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Cleans up slave and devices after the testrun",
+    "step_identifier": "testrun_cleanup",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.deallocator",
+        "class_name": "DeviceDeallocatorStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+        "class_name": "CleanupWorkdirStepFactory"
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/kir/tests/bling/test_defs.json b/lab_system/master/config/kir/tests/bling/test_defs.json
new file mode 100644
index 0000000..1b4f0dc
--- /dev/null
+++ b/lab_system/master/config/kir/tests/bling/test_defs.json
@@ -0,0 +1,33 @@
+[
+  {
+    "group_name": "bling_cast",
+    "slave_name": "bling_cast_end_to_end",
+    "test_name": "bling_cast_end_to_end",
+    "identifier": "bling_cast_end_to_end",
+    "scheduler": "none",
+    "device_type": "eureka",
+    "device_group": "bling_cast",
+    "branch": "release",
+    "step_factories": [
+      {
+        "step_description": "Sets up slave and devices for the testrun.",
+        "step_identifier": "testrun_setup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+     },
+     {
+       "step_description": "Processes the output of the test",
+       "step_identifier": "output_processing",
+       "step_factories": [
+         {
+           "module_path": "lab_system.master.steps.factory.output.logs",
+           "class_name": "CopyLogsToRepositoryStepFactory"
+         }
+       ]
+    }]
+  }
+]
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/acer_core_i5_2450m_intelhd3000_win7.cfg b/lab_system/master/config/kir/tests/chrome_mirroring/acer_core_i5_2450m_intelhd3000_win7.cfg
new file mode 100644
index 0000000..a0fe8b8
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/acer_core_i5_2450m_intelhd3000_win7.cfg
@@ -0,0 +1,19 @@
+# Config file for acer_core_i5 in MTV lab.
+
+# PROPERTIES
+SELF_IP=172.17.23.194
+HOME_DIR=C:/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver.exe
+PLATFORM=win
+
+# Mirroring test chrome build
+CHROME_VERSION=beta
+
+# Mirroring test extension flavor
+EXTENSION_BUILD=beta
+
+# Recorder machine
+SENDER_DEVICE_TYPE=mac
+SENDER_DEVICE_GROUP=video_recorder
+SENDER_DEVICE_NUMBER=1
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/config_files_on_slaves.json b/lab_system/master/config/kir/tests/chrome_mirroring/config_files_on_slaves.json
new file mode 100755
index 0000000..7db27dc
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/config_files_on_slaves.json
@@ -0,0 +1,61 @@
+{
+    "macbookair_i5_4250u_intelhd5000": {
+        "cfg_files": [
+            "soccer_30fps_encoded_v2",
+            "gangnam_encoded_v2",
+            "gangnam_encoded_v2_bad_wifi"
+        ]
+    },
+    "macbookpro_i7_2675qm_radeon6750m": {
+        "cfg_files": [
+            "soccer_30fps_encoded_v2",
+            "gangnam_encoded_v2",
+            "gangnam_encoded_v2_bad_wifi"
+        ]
+    },
+    "acer_i5_3337u_intelhd4000_win8": {
+        "cfg_files": [
+            "soccer_30fps_encoded_v2",
+            "gangnam_encoded_v2",
+            "gangnam_encoded_v2_bad_wifi"
+        ]
+    },
+    "samsung_i7_3630qm_radeon7730m_win8": {
+        "cfg_files": [
+            "soccer_30fps_encoded_v2",
+            "gangnam_encoded_v2",
+            "gangnam_encoded_v2_bad_wifi"
+        ]
+    },
+    "asus_i3_2328m_intelhd3000_win8": {
+        "cfg_files": [
+            "soccer_30fps_encoded_v2",
+            "gangnam_encoded_v2",
+            "gangnam_encoded_v2_bad_wifi"
+        ]
+    },
+    "macbookair_i5_4250u_intelhd5000_mtv": {
+        "cfg_files": [
+            "soccer_30fps_encoded_v2",
+            "gangnam_encoded_v2"
+        ]
+    },
+    "hp_core_i5_3210m_intelhd4000_win8_mtv": {
+        "cfg_files": [
+            "soccer_30fps_encoded_v2",
+            "gangnam_encoded_v2"
+        ]
+    },
+    "hp_core2duo_p8600_gma4500_win7": {
+        "cfg_files": [
+            "soccer_30fps_encoded_v2",
+            "gangnam_encoded_v2",
+            "gangnam_encoded_v2_bad_wifi"
+        ]
+    },
+    "trial_machine_win": {
+        "cfg_files": [
+            "gangnam_encoded_v2"
+        ]
+    }
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/hp_core_i5_3210m_intelhd4000_win8_mtv.cfg b/lab_system/master/config/kir/tests/chrome_mirroring/hp_core_i5_3210m_intelhd4000_win8_mtv.cfg
new file mode 100644
index 0000000..d6bd2ec
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/hp_core_i5_3210m_intelhd4000_win8_mtv.cfg
@@ -0,0 +1,19 @@
+# Config file for hp_core_i5_3210m_intelhd4000_win8_mtv in MTV lab.
+
+# PROPERTIES
+SELF_IP=172.17.23.78
+HOME_DIR=C:/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver.exe
+PLATFORM=win
+
+# Mirroring test chrome build
+CHROME_VERSION=beta
+
+# Mirroring test extension flavor
+EXTENSION_BUILD=beta
+
+# Recorder machine
+SENDER_DEVICE_TYPE=mac
+SENDER_DEVICE_GROUP=video_recorder
+SENDER_DEVICE_NUMBER=1
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/macbookair_i5_4250u_intelhd5000_mtv.cfg b/lab_system/master/config/kir/tests/chrome_mirroring/macbookair_i5_4250u_intelhd5000_mtv.cfg
new file mode 100644
index 0000000..1b81e61
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/macbookair_i5_4250u_intelhd5000_mtv.cfg
@@ -0,0 +1,27 @@
+# Config file for macbookair_i5_4250u_intelhd5000_mtv in MTV lab.
+
+# PROPERTIES
+SELF_IP=172.17.23.86
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver
+PLATFORM=mac
+
+# Mirroring test chrome build
+CHROME_VERSION=beta
+
+# Mirroring test extension flavor
+EXTENSION_BUILD=beta
+
+# Recorder machine
+SENDER_DEVICE_TYPE=mac
+SENDER_DEVICE_GROUP=video_recorder_mtv
+SENDER_DEVICE_NUMBER=1
+
+
+PROCESSOR_MACHINE_IP=172.31.25.28
+PROCESSOR_DEVICE_TYPE=linux
+PROCESSOR_DEVICE_GROUP=video_processing_machine_0
+PROCESSOR_DEVICE_NUMBER=1
+PROCESSOR_RESULTS_DIR=/home/gtvchrome/results
+PROCESSOR_TEST_DIR=/home/gtvchrome/repository/eureka/test
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/macbookpro_i5_3210m_intelhd4000.cfg b/lab_system/master/config/kir/tests/chrome_mirroring/macbookpro_i5_3210m_intelhd4000.cfg
new file mode 100644
index 0000000..c8437a7
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/macbookpro_i5_3210m_intelhd4000.cfg
@@ -0,0 +1,19 @@
+# Config file for macbook pro in MTV lab.
+
+# PROPERTIES
+SELF_IP=172.17.23.56
+HOME_DIR=/Users/gtvchrome
+USER_NAME=gtvchrome
+CHROME_DRIVER=chromedriver
+PLATFORM=mac
+
+# Mirroring test chrome build
+CHROME_VERSION=stable
+
+# Mirroring test extension flavor
+EXTENSION_BUILD=beta
+
+# Recorder machine
+SENDER_DEVICE_TYPE=mac
+SENDER_DEVICE_GROUP=video_recorder
+SENDER_DEVICE_NUMBER=1
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/bad_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/bad_wifi.config
new file mode 100644
index 0000000..a7bd44e
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/bad_wifi.config
@@ -0,0 +1,4 @@
+{
+    "udp_proxy_server": "http://172.31.25.18:8888",
+    "network_profile": "bad"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/bunny_480p_desktop_mirror.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/bunny_480p_desktop_mirror.config
new file mode 100644
index 0000000..e16eb0e
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/bunny_480p_desktop_mirror.config
@@ -0,0 +1,29 @@
+{
+    "add_debug_dongle_ip": "172.31.25.7",
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "resolution_1280_720": "1280x720",
+    "device_ip": "172.31.25.7",
+    "enable_webrtc_getstats": "true",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": " --no-proxy-server --start-maximized --whitelisted-extension-id=hfaagokkkhdbgiakmmlclaapfelnkoah --disable-web-security --enable-experimental-extension-apis --enable-usermedia-screen-capturing --enable-logging --v=1 --vmodule=*source*/talk/*=3",
+    "full_screen": "on",
+    "get_log": "true",
+    "gpu": "on",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "machine_name": "acer_i5_3337u_intelhd4000_win8",
+    "mirror_duration": "560",
+    "network_profile": "",
+    "set_fullscreen_zoom": "on",
+    "startup_latency": "false",
+    "test_type": "video",
+    "tracing_duration": "560",
+    "video_fps": "24fps",
+    "video_name": "bunny_video_24fps_480p_desktop_mirroring",
+    "video_resolution": "480p",
+    "comments": "Buffered mode, NACK on",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=bunny_480p",
+    "desktop_capture": "true"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/gangnam_encoded.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/gangnam_encoded.config
new file mode 100644
index 0000000..0de4c5e
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/gangnam_encoded.config
@@ -0,0 +1,33 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --enable-logging --v=1 --vmodule=*libjingle/source_internal/talk/*=4,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "acer_i5_3337u_intelhd4000_win8",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v1, video content 720p 24fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "1.0",
+    "cast_streaming_off": "useCastStreaming-off"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/gangnam_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/gangnam_encoded_v2.config
new file mode 100644
index 0000000..18afbc7
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/gangnam_encoded_v2.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=enhhojjnijigcajfphajepfemndkmdlo --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "acer_i5_3337u_intelhd4000_win8",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/gangnam_encoded_v2_bad_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/gangnam_encoded_v2_bad_wifi.config
new file mode 100644
index 0000000..84e4c42
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/gangnam_encoded_v2_bad_wifi.config
@@ -0,0 +1,33 @@
+{
+    "_deps": ["processor.config", "bad_wifi.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "acer_i5_3337u_intelhd4000_win8",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, bad wifi",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/processor.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/processor.config
new file mode 100644
index 0000000..0e8eaf1
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/processor.config
@@ -0,0 +1,24 @@
+{
+    "root_dir": "/home/gtvchrome",
+    "results_dir": "results",
+    "mirror_mp4_file": "Segment_0001.mp4",
+    "mirror_yuv_file": "mirror.yuv",
+    "mirror_wav_file": "mirror.wav",
+    "mirror_stats_file": "mirror_stats.txt",
+    "mirror_audio_results_file": "audio_results.txt",
+    "mirror_psnr_ssim_file": "psnr_ssim.txt",
+    "audio_tools_dir": "audio_quality_tools",
+    "image_tools_dir": "image_quality_tools",
+    "reference_dir": "image_quality_tools/sample_videos",
+    "test_utils_dir": "repository/eureka/test/utilities",
+    "zxing_dir": "image_quality_tools/zxing/cpp/build",
+    "barcode_tools_dir": "repository/eureka/test/barcode_tools",
+    "frame_width": "1280",
+    "frame_height": "720",
+    "barcode_height": "32",
+    "image_extract_interval": "1",
+    "ffmpeg": "ffmpeg",
+    "sox": "sox",
+    "audio_compare_tool": "polqa/PolqaOem64"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/soccer_30fps_encoded_60fps_capture_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/soccer_30fps_encoded_60fps_capture_v2.config
new file mode 100644
index 0000000..205d914
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/soccer_30fps_encoded_60fps_capture_v2.config
@@ -0,0 +1,35 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=enhhojjnijigcajfphajepfemndkmdlo --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "acer_i5_3337u_intelhd4000_win8",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, capture at 60fps,  ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/soccer_30fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/soccer_30fps_encoded_v2.config
new file mode 100644
index 0000000..3b87592
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/soccer_30fps_encoded_v2.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=enhhojjnijigcajfphajepfemndkmdlo --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "acer_i5_3337u_intelhd4000_win8",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/titanfall_60fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/titanfall_60fps_encoded_v2.config
new file mode 100644
index 0000000..04fbf26
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/acer_i5_3337u_intelhd4000_win8/config/titanfall_60fps_encoded_v2.config
@@ -0,0 +1,35 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=enhhojjnijigcajfphajepfemndkmdlo --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "acer_i5_3337u_intelhd4000_win8",
+    "video_fps": "60",
+    "video_name": "titanfall_60fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 60fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=titanfall_60fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/bad_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/bad_wifi.config
new file mode 100644
index 0000000..a7bd44e
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/bad_wifi.config
@@ -0,0 +1,4 @@
+{
+    "udp_proxy_server": "http://172.31.25.18:8888",
+    "network_profile": "bad"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/bunny_480p_desktop_mirror.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/bunny_480p_desktop_mirror.config
new file mode 100644
index 0000000..39fb7f5
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/bunny_480p_desktop_mirror.config
@@ -0,0 +1,29 @@
+{
+    "add_debug_dongle_ip": "172.31.25.24",
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "resolution_1280_720": "1280x720",
+    "device_ip": "172.31.25.24",
+    "enable_webrtc_getstats": "true",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": " --no-proxy-server --start-maximized --whitelisted-extension-id=hfaagokkkhdbgiakmmlclaapfelnkoah --disable-web-security --enable-experimental-extension-apis --enable-usermedia-screen-capturing --enable-logging --v=1 --vmodule=*source*/talk/*=3",
+    "full_screen": "on",
+    "get_log": "true",
+    "gpu": "on",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "machine_name": "asus_i3_2328m_intelhd3000_win8",
+    "mirror_duration": "560",
+    "network_profile": "",
+    "set_fullscreen_zoom": "on",
+    "startup_latency": "false",
+    "test_type": "video",
+    "tracing_duration": "560",
+    "video_fps": "24fps",
+    "video_name": "bunny_video_24fps_480p_default_desktop_mirroring",
+    "video_resolution": "480p",
+    "comments": "Buffered mode, NACK on",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=bunny_480p",
+    "desktop_capture": "true"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/gangnam_encoded.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/gangnam_encoded.config
new file mode 100644
index 0000000..fc2592c
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/gangnam_encoded.config
@@ -0,0 +1,33 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --enable-logging --v=1 --vmodule=*libjingle/source_internal/talk/*=4,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "asus_i3_2328m_intelhd3000_win8",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v1, video content 720p 24fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "1.0",
+    "cast_streaming_off": "useCastStreaming-off"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/gangnam_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/gangnam_encoded_v2.config
new file mode 100644
index 0000000..1cbe267
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/gangnam_encoded_v2.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "asus_i3_2328m_intelhd3000_win8",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/gangnam_encoded_v2_bad_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/gangnam_encoded_v2_bad_wifi.config
new file mode 100644
index 0000000..a299e94
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/gangnam_encoded_v2_bad_wifi.config
@@ -0,0 +1,33 @@
+{
+    "_deps": ["processor.config", "bad_wifi.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "asus_i3_2328m_intelhd3000_win8",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, bad wifi",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/gangnam_encoded_v2_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/gangnam_encoded_v2_wifi.config
new file mode 100644
index 0000000..b6c9009
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/gangnam_encoded_v2_wifi.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config", "bad_wifi.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "asus_i3_2328m_intelhd3000_win8",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, good wifi",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/processor.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/processor.config
new file mode 100644
index 0000000..0e8eaf1
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/processor.config
@@ -0,0 +1,24 @@
+{
+    "root_dir": "/home/gtvchrome",
+    "results_dir": "results",
+    "mirror_mp4_file": "Segment_0001.mp4",
+    "mirror_yuv_file": "mirror.yuv",
+    "mirror_wav_file": "mirror.wav",
+    "mirror_stats_file": "mirror_stats.txt",
+    "mirror_audio_results_file": "audio_results.txt",
+    "mirror_psnr_ssim_file": "psnr_ssim.txt",
+    "audio_tools_dir": "audio_quality_tools",
+    "image_tools_dir": "image_quality_tools",
+    "reference_dir": "image_quality_tools/sample_videos",
+    "test_utils_dir": "repository/eureka/test/utilities",
+    "zxing_dir": "image_quality_tools/zxing/cpp/build",
+    "barcode_tools_dir": "repository/eureka/test/barcode_tools",
+    "frame_width": "1280",
+    "frame_height": "720",
+    "barcode_height": "32",
+    "image_extract_interval": "1",
+    "ffmpeg": "ffmpeg",
+    "sox": "sox",
+    "audio_compare_tool": "polqa/PolqaOem64"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/soccer_30fps_encoded_60fps_capture_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/soccer_30fps_encoded_60fps_capture_v2.config
new file mode 100644
index 0000000..6931d93
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/soccer_30fps_encoded_60fps_capture_v2.config
@@ -0,0 +1,35 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "asus_i3_2328m_intelhd3000_win8",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded_60fps_capture",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, capture at 60fps,  ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/soccer_30fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/soccer_30fps_encoded_v2.config
new file mode 100644
index 0000000..ae09d7b
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/soccer_30fps_encoded_v2.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "asus_i3_2328m_intelhd3000_win8",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/titanfall_60fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/titanfall_60fps_encoded_v2.config
new file mode 100644
index 0000000..31fb4b8
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/asus_i3_2328m_intelhd3000_win8/config/titanfall_60fps_encoded_v2.config
@@ -0,0 +1,35 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "asus_i3_2328m_intelhd3000_win8",
+    "video_fps": "60",
+    "video_name": "titanfall_60fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 60fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=titanfall_60fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/bad_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/bad_wifi.config
new file mode 100644
index 0000000..a7bd44e
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/bad_wifi.config
@@ -0,0 +1,4 @@
+{
+    "udp_proxy_server": "http://172.31.25.18:8888",
+    "network_profile": "bad"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/bunny_480p_desktop_mirror.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/bunny_480p_desktop_mirror.config
new file mode 100644
index 0000000..8d2e883
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/bunny_480p_desktop_mirror.config
@@ -0,0 +1,29 @@
+{
+    "add_debug_dongle_ip": "172.31.25.1",
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "resolution_1280_720": "1280x720",
+    "device_ip": "172.31.25.1",
+    "enable_webrtc_getstats": "true",
+    "extension_path": "C:\\Users\\gtvchrome\\extension_dev\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=hfaagokkkhdbgiakmmlclaapfelnkoah --disable-web-security --enable-experimental-extension-apis --enable-usermedia-screen-capturing --enable-logging --v=1 --vmodule=*source*/talk/*=3",
+    "full_screen": "on",
+    "get_log": "true",
+    "gpu": "on",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "machine_name": "hp_core2duo_p8600_gma4500_win7",
+    "mirror_duration": "560",
+    "network_profile": "",
+    "set_fullscreen_zoom": "on",
+    "startup_latency": "false",
+    "test_type": "video",
+    "tracing_duration": "560",
+    "video_fps": "24fps",
+    "video_name": "bunny_video_24fps_480p_default_desktop_mirroring",
+    "video_resolution": "480p",
+    "comments": "Buffered mode, NACK on",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=bunny_480p",
+    "desktop_capture": "true"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/gangnam_encoded.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/gangnam_encoded.config
new file mode 100644
index 0000000..5e0beed
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/gangnam_encoded.config
@@ -0,0 +1,33 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --enable-logging --v=1 --vmodule=*libjingle/source_internal/talk/*=4,*=0",
+    "low_tab_projection_quality": "ql-low",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "hp_core2duo_p8600_gma4500_win7",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v1, video content 720p 24fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "1.0",
+    "cast_streaming_off": "useCastStreaming-off"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/gangnam_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/gangnam_encoded_v2.config
new file mode 100644
index 0000000..f4c4059
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/gangnam_encoded_v2.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "low_tab_projection_quality": "ql-low",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "hp_core2duo_p8600_gma4500_win7",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/gangnam_encoded_v2_bad_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/gangnam_encoded_v2_bad_wifi.config
new file mode 100644
index 0000000..ec89110
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/gangnam_encoded_v2_bad_wifi.config
@@ -0,0 +1,33 @@
+{
+    "_deps": ["processor.config", "bad_wifi.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "low_tab_projection_quality": "ql-low",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "hp_core2duo_p8600_gma4500_win7",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, bad wifi",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/processor.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/processor.config
new file mode 100644
index 0000000..0e8eaf1
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/processor.config
@@ -0,0 +1,24 @@
+{
+    "root_dir": "/home/gtvchrome",
+    "results_dir": "results",
+    "mirror_mp4_file": "Segment_0001.mp4",
+    "mirror_yuv_file": "mirror.yuv",
+    "mirror_wav_file": "mirror.wav",
+    "mirror_stats_file": "mirror_stats.txt",
+    "mirror_audio_results_file": "audio_results.txt",
+    "mirror_psnr_ssim_file": "psnr_ssim.txt",
+    "audio_tools_dir": "audio_quality_tools",
+    "image_tools_dir": "image_quality_tools",
+    "reference_dir": "image_quality_tools/sample_videos",
+    "test_utils_dir": "repository/eureka/test/utilities",
+    "zxing_dir": "image_quality_tools/zxing/cpp/build",
+    "barcode_tools_dir": "repository/eureka/test/barcode_tools",
+    "frame_width": "1280",
+    "frame_height": "720",
+    "barcode_height": "32",
+    "image_extract_interval": "1",
+    "ffmpeg": "ffmpeg",
+    "sox": "sox",
+    "audio_compare_tool": "polqa/PolqaOem64"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/soccer_30fps_encoded_60fps_capture_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/soccer_30fps_encoded_60fps_capture_v2.config
new file mode 100644
index 0000000..37c4768
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/soccer_30fps_encoded_60fps_capture_v2.config
@@ -0,0 +1,35 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "low_tab_projection_quality": "ql-low",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "hp_core2duo_p8600_gma4500_win7",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, capture at 60fps,  ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/soccer_30fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/soccer_30fps_encoded_v2.config
new file mode 100644
index 0000000..bb689df
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/soccer_30fps_encoded_v2.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "low_tab_projection_quality": "ql-low",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "hp_core2duo_p8600_gma4500_win7",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/titanfall_60fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/titanfall_60fps_encoded_v2.config
new file mode 100644
index 0000000..2b22d26
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core2duo_p8600_gma4500_win7/config/titanfall_60fps_encoded_v2.config
@@ -0,0 +1,35 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "low_tab_projection_quality": "ql-low",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "hp_core2duo_p8600_gma4500_win7",
+    "video_fps": "60",
+    "video_name": "titanfall_60fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 60fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=titanfall_60fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/gangnam_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/gangnam_encoded_v2.config
new file mode 100644
index 0000000..dcce4ea
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/gangnam_encoded_v2.config
@@ -0,0 +1,31 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=dliochdbjfkdbacpmhlcpmleaejidimm --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "full_screen": "on",
+    "gpu": "on",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "hp_core_i5_3210m_intelhd4000_win8_mtv",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, ethernet",
+    "video_site": "http://172.17.23.200/perf_test/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/processor.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/processor.config
new file mode 100644
index 0000000..0e8eaf1
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/processor.config
@@ -0,0 +1,24 @@
+{
+    "root_dir": "/home/gtvchrome",
+    "results_dir": "results",
+    "mirror_mp4_file": "Segment_0001.mp4",
+    "mirror_yuv_file": "mirror.yuv",
+    "mirror_wav_file": "mirror.wav",
+    "mirror_stats_file": "mirror_stats.txt",
+    "mirror_audio_results_file": "audio_results.txt",
+    "mirror_psnr_ssim_file": "psnr_ssim.txt",
+    "audio_tools_dir": "audio_quality_tools",
+    "image_tools_dir": "image_quality_tools",
+    "reference_dir": "image_quality_tools/sample_videos",
+    "test_utils_dir": "repository/eureka/test/utilities",
+    "zxing_dir": "image_quality_tools/zxing/cpp/build",
+    "barcode_tools_dir": "repository/eureka/test/barcode_tools",
+    "frame_width": "1280",
+    "frame_height": "720",
+    "barcode_height": "32",
+    "image_extract_interval": "1",
+    "ffmpeg": "ffmpeg",
+    "sox": "sox",
+    "audio_compare_tool": "polqa/PolqaOem64"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/soccer_30fps_encoded_60fps_capture_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/soccer_30fps_encoded_60fps_capture_v2.config
new file mode 100644
index 0000000..247ae6f
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/soccer_30fps_encoded_60fps_capture_v2.config
@@ -0,0 +1,32 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=dliochdbjfkdbacpmhlcpmleaejidimm --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "gpu": "on",
+    "full_screen": "on",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "hp_core_i5_3210m_intelhd4000_win8_mtv",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, capture at 60fps,  ethernet",
+    "video_site": "http://172.17.23.200/perf_test/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/soccer_30fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/soccer_30fps_encoded_v2.config
new file mode 100644
index 0000000..3d3e28d
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/soccer_30fps_encoded_v2.config
@@ -0,0 +1,31 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=dliochdbjfkdbacpmhlcpmleaejidimm --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "gpu": "on",
+    "full_screen": "on",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "hp_core_i5_3210m_intelhd4000_win8_mtv",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, ethernet",
+    "video_site": "http://172.17.23.200/perf_test/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/titanfall_60fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/titanfall_60fps_encoded_v2.config
new file mode 100644
index 0000000..273a15b
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/hp_core_i5_3210m_intelhd4000_win8_mtv/config/titanfall_60fps_encoded_v2.config
@@ -0,0 +1,32 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=dliochdbjfkdbacpmhlcpmleaejidimm --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "gpu": "on",
+    "full_screen": "on",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "hp_core_i5_3210m_intelhd4000_win8_mtv",
+    "video_fps": "60",
+    "video_name": "titanfall_60fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 60fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=titanfall_60fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/bad_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/bad_wifi.config
new file mode 100644
index 0000000..5d576b1
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/bad_wifi.config
@@ -0,0 +1,4 @@
+{
+    "udp_proxy_server": "http://172.31.25.18:8888",
+    "network_profile": "bad"
+}
\ No newline at end of file
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/bunny_480p_desktop_mirror.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/bunny_480p_desktop_mirror.config
new file mode 100644
index 0000000..ca5df9f
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/bunny_480p_desktop_mirror.config
@@ -0,0 +1,31 @@
+{
+    "add_debug_dongle_ip": "172.31.25.19",
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "resolution_1280_720": "1280x720",
+    "device_ip": "172.31.25.19",
+    "enable_webrtc_getstats": "true",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=hfaagokkkhdbgiakmmlclaapfelnkoah --disable-web-security --enable-experimental-extension-apis --enable-usermedia-screen-capturing --enable-logging --v=1 --vmodule=*libjingle/source_internal/talk/*=4,*=0",
+    "full_screen": "on",
+    "get_log": "true",
+    "gpu": "on",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "machine_name": "macbookair_i5_4250u_intelhd5000",
+    "mirror_duration": "560",
+    "network_profile": "",
+    "set_fullscreen_zoom": "on",
+    "ssh_private_key": "/Users/gtvchrome/.ssh/id_rsa",
+    "startup_latency": "false",
+    "test_type": "video",
+    "tracing_duration": "560",
+    "video_fps": "24fps",
+    "video_name": "bunny_video_24fps_480p_default_desktop_mirroring",
+    "video_resolution": "480p",
+    "comments": "Buffered mode, NACK on",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=bunny_480p",
+    "use_sdk": "true",
+    "desktop_capture": "true"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/gangnam_encoded.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/gangnam_encoded.config
new file mode 100644
index 0000000..4b80ade
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/gangnam_encoded.config
@@ -0,0 +1,33 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --enable-logging --v=1 --vmodule=*libjingle/source_internal/talk/*=4,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookair_i5_4250u_intelhd5000",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v1, video content 720p 24fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "1.0",
+    "cast_streaming_off": "useCastStreaming-off"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/gangnam_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/gangnam_encoded_v2.config
new file mode 100644
index 0000000..20d7466
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/gangnam_encoded_v2.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging=stderr --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0 --log-level=1",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookair_i5_4250u_intelhd5000",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/gangnam_encoded_v2_bad_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/gangnam_encoded_v2_bad_wifi.config
new file mode 100644
index 0000000..78a62b1
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/gangnam_encoded_v2_bad_wifi.config
@@ -0,0 +1,33 @@
+{
+    "_deps": ["processor.config", "bad_wifi.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging=stderr --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0 --log-level=1",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookair_i5_4250u_intelhd5000",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, bad wifi",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/processor.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/processor.config
new file mode 100644
index 0000000..0e8eaf1
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/processor.config
@@ -0,0 +1,24 @@
+{
+    "root_dir": "/home/gtvchrome",
+    "results_dir": "results",
+    "mirror_mp4_file": "Segment_0001.mp4",
+    "mirror_yuv_file": "mirror.yuv",
+    "mirror_wav_file": "mirror.wav",
+    "mirror_stats_file": "mirror_stats.txt",
+    "mirror_audio_results_file": "audio_results.txt",
+    "mirror_psnr_ssim_file": "psnr_ssim.txt",
+    "audio_tools_dir": "audio_quality_tools",
+    "image_tools_dir": "image_quality_tools",
+    "reference_dir": "image_quality_tools/sample_videos",
+    "test_utils_dir": "repository/eureka/test/utilities",
+    "zxing_dir": "image_quality_tools/zxing/cpp/build",
+    "barcode_tools_dir": "repository/eureka/test/barcode_tools",
+    "frame_width": "1280",
+    "frame_height": "720",
+    "barcode_height": "32",
+    "image_extract_interval": "1",
+    "ffmpeg": "ffmpeg",
+    "sox": "sox",
+    "audio_compare_tool": "polqa/PolqaOem64"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/soccer_30fps_encoded_60fps_capture_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/soccer_30fps_encoded_60fps_capture_v2.config
new file mode 100644
index 0000000..fbf6a62
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/soccer_30fps_encoded_60fps_capture_v2.config
@@ -0,0 +1,35 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging=stderr --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0 --log-level=1",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookair_i5_4250u_intelhd5000",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, capture at 60fps,  ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/soccer_30fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/soccer_30fps_encoded_v2.config
new file mode 100644
index 0000000..775dfc1
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/soccer_30fps_encoded_v2.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging=stderr --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0 --log-level=1",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookair_i5_4250u_intelhd5000",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/titanfall_60fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/titanfall_60fps_encoded_v2.config
new file mode 100644
index 0000000..06fe844
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000/config/titanfall_60fps_encoded_v2.config
@@ -0,0 +1,35 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging=stderr --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0 --log-level=1",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookair_i5_4250u_intelhd5000",
+    "video_fps": "60",
+    "video_name": "titanfall_60fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 60fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=titanfall_60fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/gangnam_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/gangnam_encoded_v2.config
new file mode 100644
index 0000000..28394de
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/gangnam_encoded_v2.config
@@ -0,0 +1,31 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=dliochdbjfkdbacpmhlcpmleaejidimm --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging=stderr --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0 --log-level=1",
+    "gpu": "on",
+    "full_screen": "on",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookair_i5_4250u_intelhd5000_mtv",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, ethernet",
+    "video_site": "http://172.17.23.200/perf_test/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/processor.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/processor.config
new file mode 100644
index 0000000..0e8eaf1
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/processor.config
@@ -0,0 +1,24 @@
+{
+    "root_dir": "/home/gtvchrome",
+    "results_dir": "results",
+    "mirror_mp4_file": "Segment_0001.mp4",
+    "mirror_yuv_file": "mirror.yuv",
+    "mirror_wav_file": "mirror.wav",
+    "mirror_stats_file": "mirror_stats.txt",
+    "mirror_audio_results_file": "audio_results.txt",
+    "mirror_psnr_ssim_file": "psnr_ssim.txt",
+    "audio_tools_dir": "audio_quality_tools",
+    "image_tools_dir": "image_quality_tools",
+    "reference_dir": "image_quality_tools/sample_videos",
+    "test_utils_dir": "repository/eureka/test/utilities",
+    "zxing_dir": "image_quality_tools/zxing/cpp/build",
+    "barcode_tools_dir": "repository/eureka/test/barcode_tools",
+    "frame_width": "1280",
+    "frame_height": "720",
+    "barcode_height": "32",
+    "image_extract_interval": "1",
+    "ffmpeg": "ffmpeg",
+    "sox": "sox",
+    "audio_compare_tool": "polqa/PolqaOem64"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/soccer_30fps_encoded_60fps_capture_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/soccer_30fps_encoded_60fps_capture_v2.config
new file mode 100644
index 0000000..6cde31b
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/soccer_30fps_encoded_60fps_capture_v2.config
@@ -0,0 +1,32 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=dliochdbjfkdbacpmhlcpmleaejidimm --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging=stderr --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0 --log-level=1",
+    "gpu": "on",
+    "full_screen": "on",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookair_i5_4250u_intelhd5000_mtv",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, capture at 60fps,  ethernet",
+    "video_site": "http://172.17.23.200/perf_test/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/soccer_30fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/soccer_30fps_encoded_v2.config
new file mode 100644
index 0000000..b082265
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/soccer_30fps_encoded_v2.config
@@ -0,0 +1,31 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=dliochdbjfkdbacpmhlcpmleaejidimm --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging=stderr --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0 --log-level=1",
+    "gpu": "on",
+    "full_screen": "on",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookair_i5_4250u_intelhd5000_mtv",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, ethernet",
+    "video_site": "http://172.17.23.200/perf_test/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/titanfall_60fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/titanfall_60fps_encoded_v2.config
new file mode 100644
index 0000000..821fa4b
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookair_i5_4250u_intelhd5000_mtv/config/titanfall_60fps_encoded_v2.config
@@ -0,0 +1,32 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=dliochdbjfkdbacpmhlcpmleaejidimm --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging=stderr --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0 --log-level=1",
+    "gpu": "on",
+    "full_screen": "on",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookair_i5_4250u_intelhd5000_mtv",
+    "video_fps": "60",
+    "video_name": "titanfall_60fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 60fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=titanfall_60fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/bad_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/bad_wifi.config
new file mode 100644
index 0000000..5d576b1
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/bad_wifi.config
@@ -0,0 +1,4 @@
+{
+    "udp_proxy_server": "http://172.31.25.18:8888",
+    "network_profile": "bad"
+}
\ No newline at end of file
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/bunny_480p_desktop_mirror.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/bunny_480p_desktop_mirror.config
new file mode 100644
index 0000000..204a2a4
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/bunny_480p_desktop_mirror.config
@@ -0,0 +1,30 @@
+{
+    "add_debug_dongle_ip": "172.31.25.8",
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "resolution_1280_720": "1280x720",
+    "device_ip": "172.31.25.8",
+    "enable_webrtc_getstats": "true",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=hfaagokkkhdbgiakmmlclaapfelnkoah --disable-web-security --enable-experimental-extension-apis --enable-usermedia-screen-capturing --enable-logging --v=1 --vmodule=*libjingle/source_internal/talk/*=4,*=0",
+    "full_screen": "on",
+    "get_log": "true",
+    "gpu": "on",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "machine_name": "macbookpro_i7_2675qm_radeon6750m",
+    "mirror_duration": "560",
+    "network_profile": "",
+    "set_fullscreen_zoom": "on",
+    "ssh_private_key": "/Users/gtvchrome/.ssh/id_rsa",
+    "startup_latency": "false",
+    "test_type": "video",
+    "tracing_duration": "560",
+    "video_fps": "24fps",
+    "video_name": "bunny_video_24fps_480p_default_desktop_mirroring",
+    "video_resolution": "480p",
+    "comments": "Buffered mode, NACK on",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=bunny_480p",
+    "desktop_capture": "true"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/gangnam_encoded.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/gangnam_encoded.config
new file mode 100644
index 0000000..eeedcb5
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/gangnam_encoded.config
@@ -0,0 +1,33 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --enable-logging --v=1 --vmodule=*libjingle/source_internal/talk/*=4,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookpro_i7_2675qm_radeon6750m",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v1, video content 720p 24fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "1.0",
+    "cast_streaming_off": "useCastStreaming-off"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/gangnam_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/gangnam_encoded_v2.config
new file mode 100644
index 0000000..99e880b
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/gangnam_encoded_v2.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging=stderr --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0 --log-level=1",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookpro_i7_2675qm_radeon6750m",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/gangnam_encoded_v2_bad_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/gangnam_encoded_v2_bad_wifi.config
new file mode 100644
index 0000000..949a234
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/gangnam_encoded_v2_bad_wifi.config
@@ -0,0 +1,33 @@
+{
+    "_deps": ["processor.config", "bad_wifi.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging=stderr --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0 --log-level=1",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookpro_i7_2675qm_radeon6750m",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, bad wifi",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/processor.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/processor.config
new file mode 100644
index 0000000..0e8eaf1
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/processor.config
@@ -0,0 +1,24 @@
+{
+    "root_dir": "/home/gtvchrome",
+    "results_dir": "results",
+    "mirror_mp4_file": "Segment_0001.mp4",
+    "mirror_yuv_file": "mirror.yuv",
+    "mirror_wav_file": "mirror.wav",
+    "mirror_stats_file": "mirror_stats.txt",
+    "mirror_audio_results_file": "audio_results.txt",
+    "mirror_psnr_ssim_file": "psnr_ssim.txt",
+    "audio_tools_dir": "audio_quality_tools",
+    "image_tools_dir": "image_quality_tools",
+    "reference_dir": "image_quality_tools/sample_videos",
+    "test_utils_dir": "repository/eureka/test/utilities",
+    "zxing_dir": "image_quality_tools/zxing/cpp/build",
+    "barcode_tools_dir": "repository/eureka/test/barcode_tools",
+    "frame_width": "1280",
+    "frame_height": "720",
+    "barcode_height": "32",
+    "image_extract_interval": "1",
+    "ffmpeg": "ffmpeg",
+    "sox": "sox",
+    "audio_compare_tool": "polqa/PolqaOem64"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/soccer_30fps_encoded_60fps_capture_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/soccer_30fps_encoded_60fps_capture_v2.config
new file mode 100644
index 0000000..70dc55a
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/soccer_30fps_encoded_60fps_capture_v2.config
@@ -0,0 +1,35 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging=stderr --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0 --log-level=1",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookpro_i7_2675qm_radeon6750m",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, capture at 60fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/soccer_30fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/soccer_30fps_encoded_v2.config
new file mode 100644
index 0000000..11d9535
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/soccer_30fps_encoded_v2.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging=stderr --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0 --log-level=1",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookpro_i7_2675qm_radeon6750m",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/titanfall_60fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/titanfall_60fps_encoded_v2.config
new file mode 100644
index 0000000..3f9e04e
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/macbookpro_i7_2675qm_radeon6750m/config/titanfall_60fps_encoded_v2.config
@@ -0,0 +1,35 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "/Users/gtvchrome/repository/chrome/src/out/Debug/Google Chrome.app/Contents/MacOS/Google Chrome",
+    "extension_path": "/Users/gtvchrome/repository/extension/extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging=stderr --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0 --log-level=1",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "/Users/gtvchrome/results/sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "macbookpro_i7_2675qm_radeon6750m",
+    "video_fps": "60",
+    "video_name": "titanfall_60fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 60fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=titanfall_60fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/bad_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/bad_wifi.config
new file mode 100644
index 0000000..a7bd44e
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/bad_wifi.config
@@ -0,0 +1,4 @@
+{
+    "udp_proxy_server": "http://172.31.25.18:8888",
+    "network_profile": "bad"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/bunny_480p_desktop_mirror.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/bunny_480p_desktop_mirror.config
new file mode 100644
index 0000000..f65cfe0
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/bunny_480p_desktop_mirror.config
@@ -0,0 +1,29 @@
+{
+    "add_debug_dongle_ip": "172.31.25.10",
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "resolution_1280_720": "1280x720",
+    "device_ip": "172.31.25.10",
+    "enable_webrtc_getstats": "true",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=hfaagokkkhdbgiakmmlclaapfelnkoah --disable-web-security --enable-experimental-extension-apis --enable-usermedia-screen-capturing --enable-logging --v=1 --vmodule=*source*/talk/*=3",
+    "full_screen": "on",
+    "get_log": "true",
+    "gpu": "on",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "machine_name": "samsung_i7_3630qm_radeon7730m_win8",
+    "mirror_duration": "560",
+    "network_profile": "",
+    "set_fullscreen_zoom": "on",
+    "startup_latency": "false",
+    "test_type": "video",
+    "tracing_duration": "560",
+    "video_fps": "24fps",
+    "video_name": "bunny_video_24fps_480p_default_desktop_mirroring",
+    "video_resolution": "480p",
+    "comments": "Buffered mode, NACK on",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=bunny_480p",
+    "desktop_capture": "true"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/gangnam_encoded.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/gangnam_encoded.config
new file mode 100644
index 0000000..67dd618
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/gangnam_encoded.config
@@ -0,0 +1,33 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --enable-logging --v=1 --vmodule=*libjingle/source_internal/talk/*=4,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "samsung_i7_3630qm_radeon7730m_win8",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v1, video content 720p 24fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "1.0",
+    "cast_streaming_off": "useCastStreaming-off"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/gangnam_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/gangnam_encoded_v2.config
new file mode 100644
index 0000000..ca64170
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/gangnam_encoded_v2.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config", "../../common/common_win.cfg"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=enhhojjnijigcajfphajepfemndkmdlo --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "samsung_i7_3630qm_radeon7730m_win8",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/gangnam_encoded_v2_bad_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/gangnam_encoded_v2_bad_wifi.config
new file mode 100644
index 0000000..10a3d00
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/gangnam_encoded_v2_bad_wifi.config
@@ -0,0 +1,33 @@
+{
+    "_deps": ["processor.config", "bad_wifi.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=enhhojjnijigcajfphajepfemndkmdlo --disable-web-security --enable-experimental-extension-apis --v=1 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "samsung_i7_3630qm_radeon7730m_win8",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, bad wifi",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/processor.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/processor.config
new file mode 100644
index 0000000..0e8eaf1
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/processor.config
@@ -0,0 +1,24 @@
+{
+    "root_dir": "/home/gtvchrome",
+    "results_dir": "results",
+    "mirror_mp4_file": "Segment_0001.mp4",
+    "mirror_yuv_file": "mirror.yuv",
+    "mirror_wav_file": "mirror.wav",
+    "mirror_stats_file": "mirror_stats.txt",
+    "mirror_audio_results_file": "audio_results.txt",
+    "mirror_psnr_ssim_file": "psnr_ssim.txt",
+    "audio_tools_dir": "audio_quality_tools",
+    "image_tools_dir": "image_quality_tools",
+    "reference_dir": "image_quality_tools/sample_videos",
+    "test_utils_dir": "repository/eureka/test/utilities",
+    "zxing_dir": "image_quality_tools/zxing/cpp/build",
+    "barcode_tools_dir": "repository/eureka/test/barcode_tools",
+    "frame_width": "1280",
+    "frame_height": "720",
+    "barcode_height": "32",
+    "image_extract_interval": "1",
+    "ffmpeg": "ffmpeg",
+    "sox": "sox",
+    "audio_compare_tool": "polqa/PolqaOem64"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/soccer_30fps_encoded_60fps_capture_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/soccer_30fps_encoded_60fps_capture_v2.config
new file mode 100644
index 0000000..d27f487
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/soccer_30fps_encoded_60fps_capture_v2.config
@@ -0,0 +1,35 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=enhhojjnijigcajfphajepfemndkmdlo --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "samsung_i7_3630qm_radeon7730m_win8",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, capture at 60fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/soccer_30fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/soccer_30fps_encoded_v2.config
new file mode 100644
index 0000000..d3f86a9
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/soccer_30fps_encoded_v2.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=enhhojjnijigcajfphajepfemndkmdlo --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "samsung_i7_3630qm_radeon7730m_win8",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/titanfall_60fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/titanfall_60fps_encoded_v2.config
new file mode 100644
index 0000000..bea1894
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/samsung_i7_3630qm_radeon7730m_win8/config/titanfall_60fps_encoded_v2.config
@@ -0,0 +1,35 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=enhhojjnijigcajfphajepfemndkmdlo --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "samsung_i7_3630qm_radeon7730m_win8",
+    "video_fps": "60",
+    "video_name": "titanfall_60fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 60fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=titanfall_60fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on",
+    "max_frame_rate": "60"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/bad_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/bad_wifi.config
new file mode 100755
index 0000000..95541c2
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/bad_wifi.config
@@ -0,0 +1,4 @@
+{
+    "udp_proxy_ip": "172.31.25.18",
+    "network_profile": "bad"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/gangnam_encoded.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/gangnam_encoded.config
new file mode 100755
index 0000000..3a99cbb
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/gangnam_encoded.config
@@ -0,0 +1,33 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=hfaagokkkhdbgiakmmlclaapfelnkoah --disable-web-security --enable-experimental-extension-apis --enable-logging --v=1 --vmodule=*libjingle/source_internal/talk/*=4,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "acer_i5_3337u_intelhd4000_win8",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v1, video content 720p 24fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "1.0",
+    "cast_streaming_off": "useCastStreaming-off"
+}
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/gangnam_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/gangnam_encoded_v2.config
new file mode 100755
index 0000000..97ee1a9
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/gangnam_encoded_v2.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=fmfcbgogabcbclcofgocippekhfcmgfj --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "acer_i5_3337u_intelhd4000_win8",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/gangnam_encoded_v2_bad_wifi.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/gangnam_encoded_v2_bad_wifi.config
new file mode 100755
index 0000000..50ebb3d
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/gangnam_encoded_v2_bad_wifi.config
@@ -0,0 +1,33 @@
+{
+    "_deps": ["processor.config", "bad_wifi.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=hfaagokkkhdbgiakmmlclaapfelnkoah --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "acer_i5_3337u_intelhd4000_win8",
+    "video_fps": "24",
+    "video_name": "gangnam_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 24fps, bad wifi",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=gangnam_720p_av_test",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "false",
+    "quality_test": "all",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/processor.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/processor.config
new file mode 100755
index 0000000..0e8eaf1
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/processor.config
@@ -0,0 +1,24 @@
+{
+    "root_dir": "/home/gtvchrome",
+    "results_dir": "results",
+    "mirror_mp4_file": "Segment_0001.mp4",
+    "mirror_yuv_file": "mirror.yuv",
+    "mirror_wav_file": "mirror.wav",
+    "mirror_stats_file": "mirror_stats.txt",
+    "mirror_audio_results_file": "audio_results.txt",
+    "mirror_psnr_ssim_file": "psnr_ssim.txt",
+    "audio_tools_dir": "audio_quality_tools",
+    "image_tools_dir": "image_quality_tools",
+    "reference_dir": "image_quality_tools/sample_videos",
+    "test_utils_dir": "repository/eureka/test/utilities",
+    "zxing_dir": "image_quality_tools/zxing/cpp/build",
+    "barcode_tools_dir": "repository/eureka/test/barcode_tools",
+    "frame_width": "1280",
+    "frame_height": "720",
+    "barcode_height": "32",
+    "image_extract_interval": "1",
+    "ffmpeg": "ffmpeg",
+    "sox": "sox",
+    "audio_compare_tool": "polqa/PolqaOem64"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/soccer_30fps_encoded_v2.config b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/soccer_30fps_encoded_v2.config
new file mode 100755
index 0000000..312e49c
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/slaves/trial_machine_win/config/soccer_30fps_encoded_v2.config
@@ -0,0 +1,34 @@
+{
+    "_deps": ["processor.config"],
+    "chrome_binary_location": "C:\\Users\\gtvchrome\\repository\\chrome\\src\\out\\Debug\\chrome.exe",
+    "extension_path": "C:\\Users\\gtvchrome\\repository\\extension\\extension.crx",
+    "extra_flags": "--no-proxy-server --start-maximized --whitelisted-extension-id=hfaagokkkhdbgiakmmlclaapfelnkoah --disable-web-security --enable-experimental-extension-apis --v=0 --enable-logging --vmodule=*media/cast/*=3,*chrome/renderer/media/*=3,*=0",
+    "high_tab_projection_quality": "ql-high",
+    "gpu": "on",
+    "full_screen": "on",
+    "set_fullscreen_zoom": "off",
+    "get_log": "true",
+    "log_file_name": "C:\\Users\\gtvchrome\\results\\sender.json",
+    "logging": "mirroring",
+    "enable_webrtc_getstats": "true",
+    "mirror_duration": "400",
+    "tracing_duration": "400",
+    "machine_name": "acer_i5_3337u_intelhd4000_win8",
+    "video_fps": "30",
+    "video_name": "soccer_30fps_encoded",
+    "video_resolution": "720p",
+    "comments": "mirroring v2, video content 720p 30fps, ethernet",
+    "video_site": "http://172.31.25.18/test_site/index.html?video=soccer_30fps_encoded",
+    "recorded_video_directory": "/Users/gtvchrome/Movies/Game\\ Capture\\ HD\\ Library/mirroring/",
+    "capture_software": "/Applications/Game\\ Capture\\ HD.app/Contents/MacOS/Game\\ Capture\\ HD",
+    "test_type": "video",
+    "startup_latency": "false",
+    "network_profile": "",
+    "cpu_consumption": "true",
+    "cpu_interval": "1",
+    "use_sdk": "true",
+    "quality_test": "audio",
+    "cast_mirroring_version": "2.0",
+    "cast_streaming_on": "useCastStreaming-on"
+}
+
diff --git a/lab_system/master/config/kir/tests/chrome_mirroring/test_defs.json b/lab_system/master/config/kir/tests/chrome_mirroring/test_defs.json
new file mode 100755
index 0000000..8e781a6
--- /dev/null
+++ b/lab_system/master/config/kir/tests/chrome_mirroring/test_defs.json
@@ -0,0 +1,1226 @@
+[
+  {
+    "group_name": "macbookair_i5_4250u_intelhd5000",
+    "slave_name": "macbookair_i5_4250u_intelhd5000",
+    "test_name": "chrome_mirroring",
+    "branch": "release",
+    "device_type": "eureka",
+    "device_group": "video_recorder_set_1",
+    "identifier": "macbookair_i5_4250u_intelhd5000",
+    "scheduler": "none",
+    "step_factories": [
+      {
+        "step_description": "Sets up testrun specific properties",
+        "step_identifier": "properties",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "MakeTempFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "DownloadGoodCombinationsJson"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "OverwritePropertiesStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Update Eureka build number, remove LOCAL_LOGS, update Chrome binaries and remove existing log files",
+        "step_identifier": "testrun_setup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetEurekaRecorderIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.allocator",
+            "class_name": "SenderAllocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetRecorderMachineIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "RemoveExistingLogFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.build.copy_test_repo",
+            "class_name": "CopyRepositoriesStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.chrome.download",
+            "class_name": "DownloadChromeStepFactory"
+          },
+                    {
+            "module_path": "lab_system.master.steps.factory.chrome.copy",
+            "class_name": "CopyChromeStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.download_cws_extension",
+            "class_name": "DownloadWebStoreExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.copy",
+            "class_name": "CopyExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyConfigFilesStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Runs chrome mirroring test",
+        "step_identifier": "test_running",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "ChromeMirroringStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.device.deallocator",
+            "class_name": "DeviceDeallocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.deallocator",
+            "class_name": "SenderDeallocatorStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Getting test result and them to Chrome Performance Dashboard",
+        "step_identifier": "output_processing",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyTestLogsStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultToProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "ProcessVideoStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultFromProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Cleanup",
+        "step_identifier": "testrun_cleanup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+            "class_name": "CleanupWorkdirStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "RemoveLogsOnMasterStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "macbookpro_i7_2675qm_radeon6750m",
+    "slave_name": "macbookpro_i7_2675qm_radeon6750m",
+    "test_name": "chrome_mirroring",
+    "branch": "release",
+    "device_type": "eureka",
+    "device_group": "video_recorder_set_1",
+    "identifier": "macbookpro_i7_2675qm_radeon6750m",
+    "scheduler": "none",
+    "step_factories": [
+      {
+        "step_description": "Sets up testrun specific properties",
+        "step_identifier": "properties",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "MakeTempFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "DownloadGoodCombinationsJson"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "OverwritePropertiesStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Update Eureka build number, remove LOCAL_LOGS, update Chrome binaries and remove existing log files",
+        "step_identifier": "testrun_setup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetEurekaRecorderIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.allocator",
+            "class_name": "SenderAllocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetRecorderMachineIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "RemoveExistingLogFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.build.copy_test_repo",
+            "class_name": "CopyRepositoriesStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.chrome.download",
+            "class_name": "DownloadChromeStepFactory"
+          },
+                    {
+            "module_path": "lab_system.master.steps.factory.chrome.copy",
+            "class_name": "CopyChromeStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.download_cws_extension",
+            "class_name": "DownloadWebStoreExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.copy",
+            "class_name": "CopyExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyConfigFilesStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Runs chrome mirroring test",
+        "step_identifier": "test_running",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "ChromeMirroringStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.device.deallocator",
+            "class_name": "DeviceDeallocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.deallocator",
+            "class_name": "SenderDeallocatorStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Getting test result and them to Chrome Performance Dashboard",
+        "step_identifier": "output_processing",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyTestLogsStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultToProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "ProcessVideoStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultFromProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Cleanup",
+        "step_identifier": "testrun_cleanup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+            "class_name": "CleanupWorkdirStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "RemoveLogsOnMasterStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "acer_i5_3337u_intelhd4000_win8",
+    "slave_name": "acer_i5_3337u_intelhd4000_win8",
+    "test_name": "chrome_mirroring",
+    "branch": "release",
+    "device_type": "eureka",
+    "device_group": "video_recorder_set_0",
+    "identifier": "acer_i5_3337u_intelhd4000_win8",
+    "scheduler": "none",
+    "step_factories": [
+      {
+        "step_description": "Sets up testrun specific properties",
+        "step_identifier": "properties",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "MakeTempFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "DownloadGoodCombinationsJson"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "OverwritePropertiesStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Update Eureka build number, remove LOCAL_LOGS, update Chrome binaries and remove existing log files",
+        "step_identifier": "testrun_setup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetEurekaRecorderIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.allocator",
+            "class_name": "SenderAllocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetRecorderMachineIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "RemoveExistingLogFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.build.copy_test_repo",
+            "class_name": "CopyRepositoriesStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.chrome.download",
+            "class_name": "DownloadChromeStepFactory"
+          },
+                    {
+            "module_path": "lab_system.master.steps.factory.chrome.copy",
+            "class_name": "CopyChromeStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.download_cws_extension",
+            "class_name": "DownloadWebStoreExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.copy",
+            "class_name": "CopyExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyConfigFilesStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Runs chrome mirroring test",
+        "step_identifier": "test_running",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "ChromeMirroringStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.device.deallocator",
+            "class_name": "DeviceDeallocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.deallocator",
+            "class_name": "SenderDeallocatorStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Getting test result and them to Chrome Performance Dashboard",
+        "step_identifier": "output_processing",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyTestLogsStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultToProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "ProcessVideoStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultFromProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Cleanup",
+        "step_identifier": "testrun_cleanup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+            "class_name": "CleanupWorkdirStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "RemoveLogsOnMasterStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "samsung_i7_3630qm_radeon7730m_win8",
+    "slave_name": "samsung_i7_3630qm_radeon7730m_win8",
+    "test_name": "chrome_mirroring",
+    "branch": "release",
+    "device_type": "eureka",
+    "device_group": "video_recorder_set_0",
+    "identifier": "samsung_i7_3630qm_radeon7730m_win8",
+    "scheduler": "none",
+    "step_factories": [
+      {
+        "step_description": "Sets up testrun specific properties",
+        "step_identifier": "properties",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "MakeTempFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "DownloadGoodCombinationsJson"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "OverwritePropertiesStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Update Eureka build number, remove LOCAL_LOGS, update Chrome binaries and remove existing log files",
+        "step_identifier": "testrun_setup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetEurekaRecorderIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.allocator",
+            "class_name": "SenderAllocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetRecorderMachineIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "RemoveExistingLogFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.build.copy_test_repo",
+            "class_name": "CopyRepositoriesStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.chrome.download",
+            "class_name": "DownloadChromeStepFactory"
+          },
+                    {
+            "module_path": "lab_system.master.steps.factory.chrome.copy",
+            "class_name": "CopyChromeStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.download_cws_extension",
+            "class_name": "DownloadWebStoreExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.copy",
+            "class_name": "CopyExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyConfigFilesStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Runs chrome mirroring test",
+        "step_identifier": "test_running",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "ChromeMirroringStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.device.deallocator",
+            "class_name": "DeviceDeallocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.deallocator",
+            "class_name": "SenderDeallocatorStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Getting test result and them to Chrome Performance Dashboard",
+        "step_identifier": "output_processing",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyTestLogsStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultToProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "ProcessVideoStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultFromProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Cleanup",
+        "step_identifier": "testrun_cleanup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+            "class_name": "CleanupWorkdirStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "RemoveLogsOnMasterStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "asus_i3_2328m_intelhd3000_win8",
+    "slave_name": "asus_i3_2328m_intelhd3000_win8",
+    "test_name": "chrome_mirroring",
+    "branch": "release",
+    "device_type": "eureka",
+    "device_group": "video_recorder_set_0",
+    "identifier": "asus_i3_2328m_intelhd3000_win8",
+    "scheduler": "none",
+    "step_factories": [
+      {
+        "step_description": "Sets up testrun specific properties",
+        "step_identifier": "properties",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "MakeTempFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "DownloadGoodCombinationsJson"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "OverwritePropertiesStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Update Eureka build number, remove LOCAL_LOGS, update Chrome binaries and remove existing log files",
+        "step_identifier": "testrun_setup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetEurekaRecorderIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.allocator",
+            "class_name": "SenderAllocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetRecorderMachineIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "RemoveExistingLogFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.build.copy_test_repo",
+            "class_name": "CopyRepositoriesStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.chrome.download",
+            "class_name": "DownloadChromeStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.chrome.copy",
+            "class_name": "CopyChromeStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.download_cws_extension",
+            "class_name": "DownloadWebStoreExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.copy",
+            "class_name": "CopyExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyConfigFilesStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Runs chrome mirroring test",
+        "step_identifier": "test_running",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "ChromeMirroringStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.device.deallocator",
+            "class_name": "DeviceDeallocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.deallocator",
+            "class_name": "SenderDeallocatorStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Getting test result and them to Chrome Performance Dashboard",
+        "step_identifier": "output_processing",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyTestLogsStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultToProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "ProcessVideoStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultFromProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Cleanup",
+        "step_identifier": "testrun_cleanup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+            "class_name": "CleanupWorkdirStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hp_core2duo_p8600_gma4500_win7",
+    "slave_name": "hp_core2duo_p8600_gma4500_win7",
+    "test_name": "chrome_mirroring",
+    "branch": "release",
+    "device_type": "eureka",
+    "device_group": "video_recorder_set_1",
+    "identifier": "hp_core2duo_p8600_gma4500_win7",
+    "scheduler": "none",
+    "step_factories": [
+      {
+        "step_description": "Sets up testrun specific properties",
+        "step_identifier": "properties",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "MakeTempFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "DownloadGoodCombinationsJson"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "OverwritePropertiesStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Update Eureka build number, remove LOCAL_LOGS, update Chrome binaries and remove existing log files",
+        "step_identifier": "testrun_setup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetEurekaRecorderIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.allocator",
+            "class_name": "SenderAllocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetRecorderMachineIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "RemoveExistingLogFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.build.copy_test_repo",
+            "class_name": "CopyRepositoriesStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.chrome.download",
+            "class_name": "DownloadChromeStepFactory"
+          },
+                    {
+            "module_path": "lab_system.master.steps.factory.chrome.copy",
+            "class_name": "CopyChromeStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.download_cws_extension",
+            "class_name": "DownloadWebStoreExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.copy",
+            "class_name": "CopyExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyConfigFilesStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Runs chrome mirroring test",
+        "step_identifier": "test_running",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "ChromeMirroringStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.device.deallocator",
+            "class_name": "DeviceDeallocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.deallocator",
+            "class_name": "SenderDeallocatorStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Getting test result and them to Chrome Performance Dashboard",
+        "step_identifier": "output_processing",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyTestLogsStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultToProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "ProcessVideoStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultFromProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Cleanup",
+        "step_identifier": "testrun_cleanup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+            "class_name": "CleanupWorkdirStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "RemoveLogsOnMasterStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "macbookair_i5_4250u_intelhd5000_mtv",
+    "slave_name": "macbookair_i5_4250u_intelhd5000_mtv",
+    "test_name": "chrome_mirroring",
+    "branch": "master",
+    "device_type": "eureka",
+    "device_group": "video_recorder_mtv",
+    "identifier": "macbookair_i5_4250u_intelhd5000_mtv",
+    "scheduler": "none",
+    "step_factories": [
+      {
+        "step_description": "Update Eureka build number, remove LOCAL_LOGS, update Chrome binaries and remove existing log files",
+        "step_identifier": "testrun_setup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetEurekaRecorderIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.allocator",
+            "class_name": "SenderAllocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetRecorderMachineIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "RemoveExistingLogFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.build.copy_test_repo",
+            "class_name": "CopyRepositoriesStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.chrome.download",
+            "class_name": "DownloadChromeStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.chrome.copy",
+            "class_name": "CopyChromeStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.download_cws_extension",
+            "class_name": "DownloadWebStoreExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.copy",
+            "class_name": "CopyExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyConfigFilesStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "SetLogs"
+          }
+        ]
+      },
+      {
+        "step_description": "Runs chrome mirroring test",
+        "step_identifier": "test_running",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "ChromeMirroringStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.device.deallocator",
+            "class_name": "DeviceDeallocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.deallocator",
+            "class_name": "SenderDeallocatorStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Getting test result and them to Chrome Performance Dashboard",
+        "step_identifier": "output_processing",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyTestLogsStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultToProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "ProcessVideoStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultFromProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Cleanup",
+        "step_identifier": "testrun_cleanup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+            "class_name": "CleanupWorkdirStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "RemoveLogsOnMasterStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hp_core_i5_3210m_intelhd4000_win8_mtv",
+    "slave_name": "hp_core_i5_3210m_intelhd4000_win8_mtv",
+    "test_name": "chrome_mirroring",
+    "branch": "master",
+    "device_type": "eureka",
+    "device_group": "video_recorder_mtv",
+    "identifier": "hp_core_i5_3210m_intelhd4000_win8_mtv",
+    "scheduler": "none",
+    "step_factories": [
+      {
+        "step_description": "Update Eureka build number, remove LOCAL_LOGS, update Chrome binaries and remove existing log files",
+        "step_identifier": "testrun_setup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetEurekaRecorderIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.allocator",
+            "class_name": "SenderAllocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetRecorderMachineIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "RemoveExistingLogFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.build.copy_test_repo",
+            "class_name": "CopyRepositoriesStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.chrome.download",
+            "class_name": "DownloadChromeStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.chrome.copy",
+            "class_name": "CopyChromeStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.download_cws_extension",
+            "class_name": "DownloadWebStoreExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.copy",
+            "class_name": "CopyExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyConfigFilesStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "SetLogs"
+          }
+        ]
+      },
+      {
+        "step_description": "Runs chrome mirroring test",
+        "step_identifier": "test_running",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "ChromeMirroringStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.device.deallocator",
+            "class_name": "DeviceDeallocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.deallocator",
+            "class_name": "SenderDeallocatorStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Getting test result and them to Chrome Performance Dashboard",
+        "step_identifier": "output_processing",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyTestLogsStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultToProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "ProcessVideoStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultFromProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Cleanup",
+        "step_identifier": "testrun_cleanup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+            "class_name": "CleanupWorkdirStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "RemoveLogsOnMasterStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "trial_machine_win",
+    "slave_name": "trial_machine_win",
+    "test_name": "chrome_mirroring",
+    "branch": "release",
+    "device_type": "eureka",
+    "device_group": "video_recorder_set_0",
+    "identifier": "trial_machine_win",
+    "scheduler": "none",
+    "step_factories": [
+      {
+        "step_description": "Sets up testrun specific properties",
+        "step_identifier": "properties",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "MakeTempFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "DownloadCustomCombinationsJson"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "OverwritePropertiesStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Fetches and installs builds on devices",
+        "step_identifier": "build_handling",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "FetchEurekaOTAPackage"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "BuildInstallerStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Update Eureka build number, remove LOCAL_LOGS, update Chrome binaries and remove existing log files",
+        "step_identifier": "testrun_setup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetEurekaRecorderIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.allocator",
+            "class_name": "SenderAllocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "SetRecorderMachineIpStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "RemoveExistingLogFiles"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.build.copy_test_repo",
+            "class_name": "CopyRepositoriesStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.chrome.download",
+            "class_name": "DownloadChromeStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.chrome.copy",
+            "class_name": "CopyChromeStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.download_cws_extension",
+            "class_name": "DownloadWebStoreExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.extension.copy",
+            "class_name": "CopyExtensionStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyConfigFilesStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Runs chrome mirroring test",
+        "step_identifier": "test_running",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "ChromeMirroringStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.device.deallocator",
+            "class_name": "DeviceDeallocatorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.sender.deallocator",
+            "class_name": "SenderDeallocatorStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Getting test result and them to Chrome Performance Dashboard",
+        "step_identifier": "output_processing",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cloudview_runners.cloudview_chrome_mirroring_runner",
+            "class_name": "CopyTestLogsStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultToProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "ProcessVideoStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "CopyResultFromProcessorStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.output.logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Cleanup",
+        "step_identifier": "testrun_cleanup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+            "class_name": "CleanupWorkdirStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.run.chrome_mirroring_runner",
+            "class_name": "RemoveLogsOnMasterStepFactory"
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/kir/tests/server/server.cfg b/lab_system/master/config/kir/tests/server/server.cfg
new file mode 100755
index 0000000..286153c
--- /dev/null
+++ b/lab_system/master/config/kir/tests/server/server.cfg
@@ -0,0 +1,11 @@
+# Config file for server tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Comma separated list defining valid IP addresses from which test results can
+# be posted:
+# RESULT_POST_IP_WHITELIST=
diff --git a/lab_system/master/config/kir/tests/server/test_defs.json b/lab_system/master/config/kir/tests/server/test_defs.json
new file mode 100755
index 0000000..915bd0d
--- /dev/null
+++ b/lab_system/master/config/kir/tests/server/test_defs.json
@@ -0,0 +1,83 @@
+[
+  {
+    "group_name": "server",
+    "slave_name": "server",
+    "test_name": "server",
+    "branch": "master",
+    "device_type": "dummy",
+    "identifier": "server",
+    "scheduler": "none",
+    "step_factories": [
+      {
+        "step_description": "Overrides property setting - not necessary for unit tests",
+        "step_identifier": "properties",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces device allocation with a no-op",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces build handling with a no-op",
+        "step_identifier": "build_handling",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces testrun setup with a no-op",
+        "step_identifier": "testrun_setup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Runs the unit tests",
+        "step_identifier": "test_running",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.server",
+            "class_name": "WebServerStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces output processing with a no-op",
+        "step_identifier": "output_processing",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces cleanup with a no-op",
+        "step_identifier": "testrun_cleanup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/kir/tests/testingrepounittests/test_defs.json b/lab_system/master/config/kir/tests/testingrepounittests/test_defs.json
new file mode 100755
index 0000000..80f54f6
--- /dev/null
+++ b/lab_system/master/config/kir/tests/testingrepounittests/test_defs.json
@@ -0,0 +1,82 @@
+[
+  {
+    "group_name": "testingrepounittests",
+    "slave_name": "testingrepounittests",
+    "test_name": "testingrepounittests",
+    "branch": "master",
+    "device_type": "dummy",
+    "identifier": "testingrepounittests",
+    "step_factories": [
+      {
+        "step_description": "Overrides property setting - not necessary for server",
+        "step_identifier": "properties",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces device allocation with a no-op",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+              "module_path": "lab_system.master.steps.factory.common.noop",
+              "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces build handling with a no-op",
+        "step_identifier": "build_handling",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces testrun setup with a no-op",
+        "step_identifier": "testrun_setup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Runs the unit tests",
+        "step_identifier": "test_running",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.unit",
+            "class_name": "UnitTestRunnerStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces output processing with a no-op",
+        "step_identifier": "output_processing",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces cleanup with a no-op",
+        "step_identifier": "testrun_cleanup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/kir/tests/testingrepounittests/testingrepounittests.cfg b/lab_system/master/config/kir/tests/testingrepounittests/testingrepounittests.cfg
new file mode 100755
index 0000000..5be2d36
--- /dev/null
+++ b/lab_system/master/config/kir/tests/testingrepounittests/testingrepounittests.cfg
@@ -0,0 +1,5 @@
+# Config file for repository unit tests
+#
+# Do not modify property names
+#
+# PROPERTIES
diff --git a/lab_system/master/config/plans/av_smokes b/lab_system/master/config/plans/av_smokes
new file mode 100644
index 0000000..d1aad6f
--- /dev/null
+++ b/lab_system/master/config/plans/av_smokes
@@ -0,0 +1,2 @@
+video_formats SupportedVideoFormatsTestCase
+audio_formats SupportedAudioFormatsTestCase
diff --git a/lab_system/master/config/plans/chirp_smoke b/lab_system/master/config/plans/chirp_smoke
new file mode 100644
index 0000000..912bc56
--- /dev/null
+++ b/lab_system/master/config/plans/chirp_smoke
@@ -0,0 +1,2 @@
+dictionary_latency_dogfood how_do_i_spell_vigorously
+assistant_playback_latency_google_play_music play_some_music_for_dancing_on_google_play_music
diff --git a/lab_system/master/config/plans/drm b/lab_system/master/config/plans/drm
new file mode 100644
index 0000000..448cd4d
--- /dev/null
+++ b/lab_system/master/config/plans/drm
@@ -0,0 +1,4 @@
+drm DrmPlayreadySmoothTestCase
+drm DrmPlayreadyTestCase
+drm DrmUnprefixedTestCase
+drm DrmTestCase
diff --git a/lab_system/master/config/plans/performance b/lab_system/master/config/plans/performance
new file mode 100644
index 0000000..88cd2c8
--- /dev/null
+++ b/lab_system/master/config/plans/performance
@@ -0,0 +1,2 @@
+canvas_benchmarks
+octane
diff --git a/lab_system/master/config/plans/smokes b/lab_system/master/config/plans/smokes
new file mode 100644
index 0000000..13ac6f6
--- /dev/null
+++ b/lab_system/master/config/plans/smokes
@@ -0,0 +1,7 @@
+audio_formats SupportedAudioFormatsTestCase
+drm DrmPlayreadySmoothTestCase
+drm DrmPlayreadyTestCase
+drm DrmUnprefixedTestCase
+drm DrmTestCase
+receiver CastReceiverComplianceTestCase
+video_formats SupportedVideoFormatsTestCase
diff --git a/lab_system/master/config/plans/uma_logging_background b/lab_system/master/config/plans/uma_logging_background
new file mode 100644
index 0000000..ea03f64
--- /dev/null
+++ b/lab_system/master/config/plans/uma_logging_background
@@ -0,0 +1,3 @@
+uma_logging UmaLoggingTestCase.testBackgroundSwappedIn
+uma_logging UmaLoggingTestCase.testBackgroundReplaced
+uma_logging UmaLoggingTestCase.testBackgroundCancelled
diff --git a/lab_system/master/config/site/site.cfg b/lab_system/master/config/site/site.cfg
new file mode 100644
index 0000000..49b9fd8
--- /dev/null
+++ b/lab_system/master/config/site/site.cfg
@@ -0,0 +1,198 @@
+# Config file for site wide values.
+#
+# These values are expected to change on a site by site basis.  If the system
+# is set up in a new location, these values would need to be changed to work
+# at that location.
+#
+# Do not modify property names
+# Run buildbot reconfig if you change a value and buildbot is running
+#
+# PROPERTIES
+
+# Path for internal unit tests.
+INTERNAL_REPOSITORY_PATH=/usr/local/google/repositories/HEAD/continuous-tests
+
+# TODO(billiani): EUREKA_TEST_DIRECTORY should be HEAD/test.
+EUREKA_TEST_DIRECTORY=/usr/local/google/repositories/HEAD
+
+#Gdb toolchain path
+GDB_PATH=/usr/local/google/repositories/HEAD/toolchain/arm-unknown-linux-gnueabi-4.5.3-glibc/bin/arm-unknown-linux-gnueabi-gdb
+
+# Usage: GET_LATEST_BUILD_SCRIPT [options]
+#
+# Options:
+#  --branch=BRANCH            branch to get latest build number
+#  --product=PRODUCT          product to get latest build number
+#  --build_output_type=TYPE   ota, symbols or tests
+#  --flavor                   eng or tests
+#  --build_number=BUILD       number of build we want the path
+#  --output_mode              path or number
+#
+# Uses to get path from a build number and to get a build number:
+# get_latest_build.py --branch=%s --product=%s --build_number=%s
+#                     --build_output_type=%s --flavor=%s --output_mode='path'
+# get_latest_build.py --branch=%s --product=%s
+#                     --build_output_type=%s --flavor=%s --output_mode='number'
+# where script that prints path or latest build number is
+GET_LATEST_BUILD_SCRIPT=/usr/local/google/repositories/HEAD/continuous-tests/build/get_latest_build.py
+
+# Root location where tests logs are stored
+RESULTS=gs://fragglerock
+
+# GCS bucket to store test result JSON files
+RESULT_JSON_BUCKET=gs://fr_json_logs
+
+# Run identifiers path
+IDS_REPOSITORY=gs://fragglerock/run_identifiers
+
+# Base URL to Google Storage console
+#GS_CONSOLE_BASE_URL=https://cloud.google.com/console#/storage/
+# Changing URL because of link redirect bug.
+GS_CONSOLE_BASE_URL=https://console.developers.google.com/storage/
+
+# Path to the base output directory in which all test data is written:
+BASE_OUTPUT_DIRECTORY=/usr/local/google/staging/fraggle_rock/output
+
+# Run environment to use for this instance.  This is used to differentiate
+# multiple instances of the system.  The value is used in several places, for
+# example when building output directory paths.  Suggested values for this are
+# parameter are: prod, staging.
+# TODO(billiani): Remove staging from PARSED, LOGS and CSV when using RUN_ENV
+RUN_ENVIRONMENT=staging
+
+# where logs are stored when tests run
+LOCAL_LOGS_DIRECTORY=/usr/local/google/staging/fraggle_rock/logs
+
+# Parsed http endpoint
+PARSED_LINK=http://hivemind.mtv.corp.google.com/eureka/parsed
+
+# CSV root dir
+CSV_ROOT=/var/www/eureka/csv
+
+# Waterfall server port
+URL_PORT=8010
+
+# Dashboard server port
+SERVER_PORT=8080
+
+# Server entry point
+SERVER_HOSTNAME=0.0.0.0
+
+# Continuous tests name
+NAME=Home Continuous Tests System
+
+# master hostname
+MASTER_HOSTNAME=home-lab-ci-master-1.mtv
+
+# Buildbot state db ip
+STATE_DB=173.194.227.201
+
+# Enables functionality that is only available within Google.  If running
+# externally, set this to 0 to ensure that all Google specific functionality is
+# not run:
+G_INTERNAL=1
+
+# Database type.
+# The currently supported backend databases are defined in db/database_types.py.
+# The default database type is Sqlite.  To use a Sqlite database:
+# Set DB_TYPE=sqlite and set DB_PATH=/path, where path is the location where
+# the Sqlite database will be stored.
+#
+# To use a PostgreSQL database:
+# Set DB_TYPE=postgresql and set DB_PATH=/path, where path is the location where
+# the PostgreSQL database configuration file exists.  An example PostgreSQL
+# configuration file is at db/db.config.
+#
+# NOTE: If using postgresql, the third party psycopg2 library must be installed
+# in your python environment.  If not, ImportErrors will occur when using any of
+# the tools that interface with the database.
+DB_TYPE=postgresql
+
+# For a PostgreSQL database backend (ie. DB_TYPE=postgresql), provide the path
+# to the PostgreSQL database configuration file (see notes for DB_TYPE above).
+#
+# For a Sqlite database backend (ie. DB_TYPE=sqlite), provide the path to the
+# Sqlite database file:
+DB_PATH=/usr/local/google/repositories/HEAD/continuous-tests/db/db.config
+
+# Password for communication between buildbot master and slaves.  This password
+# should not be set to an actual user password.  It purely governs the
+# communication between the master and slaves.
+MASTER_SLAVE_PASSWORD=buildbotmasterslavepassword
+
+# URL root for device-visible media server.  This machine is accessible to all
+# devices in the lab, and contains media, HTML and JavaScript files that are used
+# in tests.
+CNS_MEDIA_SERVER_URL=https://home-media-server.sandbox.google.com
+GCS_MEDIA_SERVER_URL=https://home-media-server-staging.sandbox.google.com
+
+# The root path of the GCS media server bucket
+GCS_ROOT_PATH=gs://home-media-server/dynamic
+
+# Path to Munin monitoring output:
+MONITOR_FILE_PATH=/var/www/mtv/hivemind.mtv/
+
+# FR6 test definition file root path.  The buildbot master looks for
+# subdirectories in this directory containing test_defs.json files.
+# These files contain the definitions of the tests to run.
+# This is an optional parameter; if it is not specified, the default test
+# definitions directory (tests/) is used.
+# FR6+ only.  If using master.cfg prior to FR6, this parameter has no effect.
+TESTS_DEFS_ROOT_PATH=tests/
+
+# Mirroring test log file storage
+REPORT_SERVER_PREFIX=https://cloud.google.com/console#/storage/fragglerock
+
+# Mirroring test status (HTML) storage
+MIRRORING_REPORT_SERVER=gtvchrome@172.17.21.60
+
+# Mirroring test results directory
+MIRRORING_REPORT_DIRECTORY=/var/www/gtv/mirroring_report
+
+# Chrome Cast Extension folder, the extension files under this folder
+# will be copied to remote machine, and the extension will be used in test.
+EXTENSION_FOLDER=/usr/local/google/home/repositories/data/extension
+
+# Test html and js files folder, it should contain all the test html and js
+# files for Chrome SDK end to end test. All the files under this folder will
+# be copied to HTTPS server during SDK end to end test.
+TEST_FILES_FOLDER=/usr/local/google/home/repositories/data/tests
+
+# Path to local directory to store chrome_sdk binary
+CHROME_SDK_TEST_DATA_FOLDER=/usr/local/google/staging/fraggle_rock/chrome_sdk
+
+# The type of access point manager used for allocation/deallocation of
+# access points. It can have one of two values "chaos" for Chrome Chaos
+# lab infrastructure or "testing" for a testing purposes access point allocator.
+ACCESS_POINT_MANAGER_TYPE=chaos
+
+# Location where build status JSON is expected:
+BUILD_STATUS_DIRECTORY_PATH=/tmp/build/status/
+
+# Port used for communication between master and slaves:
+MASTER_SLAVE_PORT_NUMBER=8014
+
+# Google Cloud project id
+CLOUD_STORAGE_PROJECT_ID=634486810642
+
+# Log pushers used to copy logs to their final destinations.
+# Currently supported types are CLOUD_STORAGE and LOCAL_LINUX
+LOG_PUSHERS=[{"type":"LOCAL_LINUX", "path": "/usr/local/google/staging/fraggle_rock/logs"}]
+
+# URL endpoint for the chromeperf dashboard for adding data. Your host must be
+# whitelisted to use the default value. A dev endpoint is available at:
+#   https://chrome-perf.googleplex.com/add_point
+# This endpoint will accept data and return a 200 if it is correctly formatted.
+CHROME_PERF_UPLOAD_URL=https://chromeperf.appspot.com/add_point
+
+# Link template to allow for referencing code from our results viewer into GoB.
+EUREKA_REPO_LINK=https://eureka-internal.git.corp.google.com/{}/+/master/{}#{}
+
+# Test repo url
+TEST_REPO_LINK=https://eureka-internal.git.corp.google.com/test/+/
+
+# Signed GmsCore APK Directory
+SIGNED_GMSCORE_DIR=/google/data/ro/teams/cast-test/gmscore/latest_signed
+
+# Android build artifact fetcher
+ANDROID_FETCH_ARTIFACT_BINARY=/google/data/ro/projects/android/fetch_artifact
diff --git a/lab_system/master/config/step_factories_recipes/android_mirroring.json b/lab_system/master/config/step_factories_recipes/android_mirroring.json
new file mode 100644
index 0000000..fe57400
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/android_mirroring.json
@@ -0,0 +1,106 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "RecordingPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Copying Build to test slave and installing on device",
+        "step_factories": [
+            {
+                "class_name": "RsyncRepositoriesStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_test_repo"
+            },
+            {
+                "class_name": "DefaultOtaFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.fetcher"
+            },
+            {
+                "class_name": "CopyBuildToSlaveStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "TurnOnSpecificDevice",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "InstallBuildStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Sets up logging",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs Android Mirroring Test",
+        "step_factories": [
+            {
+                "class_name": "AndroidMirroringStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copies local log files to Google Storage",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "Processes the output of the test",
+        "step_factories": [
+            {
+                "class_name": "GetLogcatStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "CopyLogsToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+      "step_description": "Cleans up slave and devices after the testrun",
+      "step_identifier": "testrun_cleanup",
+      "step_factories": [
+        {
+          "module_path": "lab_system.master.steps.factory.device.cloud_deallocator",
+          "class_name": "DeviceDeallocatorStepFactory"
+        },
+        {
+          "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+          "class_name": "CleanupWorkdirStepFactory"
+        }
+      ]
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/android_mirroring_cast_tv.json b/lab_system/master/config/step_factories_recipes/android_mirroring_cast_tv.json
new file mode 100644
index 0000000..f8f2405
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/android_mirroring_cast_tv.json
@@ -0,0 +1,102 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "RecordingPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Copying Build to test slave and installing on device",
+        "step_factories": [
+            {
+                "class_name": "RsyncRepositoriesStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_test_repo"
+            },
+            {
+                "class_name": "DefaultOtaFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.fetcher"
+            },
+            {
+                "class_name": "CopyBuildToSlaveStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "InstallBuildStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Sets up logging",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs Android Mirroring Test",
+        "step_factories": [
+            {
+                "class_name": "AndroidMirroringStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copies local log files to Google Storage",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "Processes the output of the test",
+        "step_factories": [
+            {
+                "class_name": "GetLogcatStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "CopyLogsToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+      "step_description": "Cleans up slave and devices after the testrun",
+      "step_identifier": "testrun_cleanup",
+      "step_factories": [
+        {
+          "module_path": "lab_system.master.steps.factory.device.deallocator",
+          "class_name": "DeviceDeallocatorStepFactory"
+        },
+        {
+          "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+          "class_name": "CleanupWorkdirStepFactory"
+        }
+      ]
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/android_mirroring_nexus_player.json b/lab_system/master/config/step_factories_recipes/android_mirroring_nexus_player.json
new file mode 100644
index 0000000..1b65ab4
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/android_mirroring_nexus_player.json
@@ -0,0 +1,102 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "RecordingPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Copying Build to test slave and installing on device",
+        "step_factories": [
+            {
+                "class_name": "RsyncRepositoriesStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_test_repo"
+            },
+            {
+                "class_name": "DefaultOtaFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.fetcher"
+            },
+            {
+                "class_name": "CopyBuildToSlaveStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "InstallBuildStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_nexus_player_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Sets up logging",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs Android Mirroring Test",
+        "step_factories": [
+            {
+                "class_name": "AndroidMirroringStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_nexus_player_runner"
+            },
+            {
+              "module_path": "lab_system.master.steps.factory.device.deallocator",
+              "class_name": "DeviceDeallocatorStepFactory"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copies local log files to Google Storage",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "Processes the output of the test",
+        "step_factories": [
+            {
+                "class_name": "GetLogcatStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "CopyLogsToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+      "step_description": "Cleans up slave and devices after the testrun",
+      "step_identifier": "testrun_cleanup",
+      "step_factories": [
+        {
+          "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+          "class_name": "CleanupWorkdirStepFactory"
+        }
+      ]
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/android_mirroring_uma.json b/lab_system/master/config/step_factories_recipes/android_mirroring_uma.json
new file mode 100644
index 0000000..cf88d84
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/android_mirroring_uma.json
@@ -0,0 +1,106 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "RecordingPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "ReducedLeaseDeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Copying Build to test slave and installing on device",
+        "step_factories": [
+            {
+                "class_name": "RsyncRepositoriesStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_test_repo"
+            },
+            {
+                "class_name": "DefaultOtaFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.fetcher"
+            },
+            {
+                "class_name": "CopyBuildToSlaveStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "InstallBuildStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Sets up logging",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs Android Mirroring Test",
+        "step_factories": [
+            {
+                "class_name": "AndroidMirroringUmaStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copies local log files to Google Storage",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "Processes the output of the test",
+        "step_factories": [
+            {
+                "class_name": "GetUmaTestLogsStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "UploadScreenCastTestResultStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "CopyLogsToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.logs"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+      "step_description": "Cleans up slave and devices after the testrun",
+      "step_identifier": "testrun_cleanup",
+      "step_factories": [
+        {
+          "module_path": "lab_system.master.steps.factory.device.cloud_deallocator",
+          "class_name": "DeviceDeallocatorStepFactory"
+        },
+        {
+          "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+          "class_name": "CleanupWorkdirStepFactory"
+        }
+      ]
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/androidthings_cast_test_recipe.json b/lab_system/master/config/step_factories_recipes/androidthings_cast_test_recipe.json
new file mode 100644
index 0000000..893827b
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/androidthings_cast_test_recipe.json
@@ -0,0 +1,142 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "AtvPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            },
+            {
+                "class_name": "AddUnitTestMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            },
+            {
+                "class_name": "GetFirstDeviceIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.allocator"
+            },
+            {
+                "class_name": "RsyncRepositoriesStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_atv_test_repo"
+            },
+            {
+                "class_name": "SetAtvIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "DownloadAndroidThingsArtifactStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.androidthings_unit_test_runner"
+            },
+            {
+                "class_name": "CopyAndroidThingsArtifactToDeviceStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.androidthings_unit_test_runner"
+            },
+            {
+                "class_name": "FlashAndroidThingsBuildToDeviceStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.androidthings_unit_test_runner"
+            },
+            {
+                "class_name": "AtvCastShellApkFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            },
+            {
+                "class_name": "CopyBuildToSlaveStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            },
+            {
+                "class_name": "InstallBuildStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            },
+            {
+                "class_name": "InstallCertAndStartCastReceiverStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.androidthings_cast_test_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Prepare Android TV",
+        "step_factories": [
+            {
+                "class_name": "StartAtvPersistentLoggingStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs cast test.",
+        "step_factories": [
+            {
+                "class_name": "AtvTestRunStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            },
+            {
+                "class_name": "GetAtvPersistentLogStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            },
+            {
+                "class_name": "GetLogcatStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            },
+            {
+                "class_name": "UploadResultToSpongeStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copy Logs",
+        "step_factories": [
+            {
+                "class_name": "CopyLogsFromTimeStampedDirToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.cloud_logs"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "Updates the good_to_test hint.",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "RemoveUnitTestMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/androidthings_unit_test_recipe.json b/lab_system/master/config/step_factories_recipes/androidthings_unit_test_recipe.json
new file mode 100644
index 0000000..78a7d35
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/androidthings_unit_test_recipe.json
@@ -0,0 +1,122 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "UnitTestTmpSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_unit_test_runner"
+            },
+            {
+                "class_name": "UnitTestPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            },
+            {
+                "class_name": "GetFirstDeviceIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.allocator"
+            },
+            {
+                "class_name": "RsyncRepositoriesStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_atv_test_repo"
+            },
+            {
+                "class_name": "SetAtvIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "BuildFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_unit_test_runner"
+            },
+            {
+                "class_name": "CopyBuildToSlaveStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_atv_test_repo"
+            },
+            {
+                "class_name": "DownloadAndroidThingsArtifactStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.androidthings_unit_test_runner"
+            },
+            {
+                "class_name": "CopyAndroidThingsArtifactToDeviceStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.androidthings_unit_test_runner"
+            },
+            {
+                "class_name": "FlashAndroidThingsBuildToDeviceStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.androidthings_unit_test_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Unzip test apk files",
+        "step_factories": [
+            {
+                "class_name": "PrepDeviceStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_unit_test_runner"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs unit test.",
+        "step_factories": [
+            {
+                "class_name": "UnitTestRunStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_unit_test_runner"
+            },
+            {
+                "class_name": "UploadResultToSpongeStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_unit_test_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copy Logs",
+        "step_factories": [
+            {
+                "class_name": "CopyLogsFromRootToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.cloud_logs"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "Updates the good_to_test hint.",
+        "step_factories": [
+            {
+                "class_name": "CreateHintStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_unit_test_runner"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/assistant_unit_test_recipe.json b/lab_system/master/config/step_factories_recipes/assistant_unit_test_recipe.json
new file mode 100644
index 0000000..9aa9b47
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/assistant_unit_test_recipe.json
@@ -0,0 +1,110 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "PropertiesSetterStepFactoryWithDummyBuild",
+                "module_path": "lab_system.master.steps.factory.setup.properties"
+            },
+            {
+                "class_name": "AddUnitTestMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "ReducedLeaseDeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            },
+            {
+                "class_name": "GetFirstDeviceIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.allocator"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "DefaultOtaFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.fetcher"
+            },
+            {
+                "class_name": "BuildInstallerWithoutFormatRollbackStartedMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Installs test binaries to unit test device",
+        "step_factories": [
+            {
+                "class_name": "PrepDeviceRollbackStartedMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs unit test.",
+        "step_factories": [
+            {
+                "class_name": "UnitTestRunStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "UploadResultToSpongeRollbackStartedMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copy log files",
+        "step_factories": [
+            {
+                "class_name": "CopyLogStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CopyLogsFromRootToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.cloud_logs"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "A placeholder with no-ops to allow custom logs parsing.",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "RemoveUnitTestMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/atv_cast_test_recipe.json b/lab_system/master/config/step_factories_recipes/atv_cast_test_recipe.json
new file mode 100644
index 0000000..4dab8fc
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/atv_cast_test_recipe.json
@@ -0,0 +1,126 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "AtvPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            },
+            {
+                "class_name": "AddUnitTestMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            },
+            {
+                "class_name": "GetFirstDeviceIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.allocator"
+            },
+            {
+                "class_name": "RsyncRepositoriesStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_atv_test_repo"
+            },
+            {
+                "class_name": "SetAtvIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "AtvCastShellApkFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            },
+            {
+                "class_name": "CopyBuildToSlaveStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            },
+            {
+                "class_name": "InstallBuildStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Prepare Android TV",
+        "step_factories": [
+            {
+                "class_name": "StartAtvPersistentLoggingStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs cast test.",
+        "step_factories": [
+            {
+                "class_name": "AtvTestRunStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            },
+            {
+                "class_name": "GetAtvPersistentLogStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            },
+            {
+                "class_name": "GetLogcatStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            },
+            {
+                "class_name": "UploadResultToSpongeStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_cast_test_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copy Logs",
+        "step_factories": [
+            {
+                "class_name": "CopyLogsFromTimeStampedDirToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.cloud_logs"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "Updates the good_to_test hint.",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "RemoveUnitTestMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/atv_unit_test_recipe.json b/lab_system/master/config/step_factories_recipes/atv_unit_test_recipe.json
new file mode 100644
index 0000000..dd3d21d
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/atv_unit_test_recipe.json
@@ -0,0 +1,106 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "UnitTestTmpSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_unit_test_runner"
+            },
+            {
+                "class_name": "UnitTestPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            },
+            {
+                "class_name": "GetFirstDeviceIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.allocator"
+            },
+            {
+                "class_name": "RsyncRepositoriesStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_atv_test_repo"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "BuildFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_unit_test_runner"
+            },
+            {
+                "class_name": "CopyBuildToSlaveStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_atv_test_repo"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Unzip test apk files",
+        "step_factories": [
+            {
+                "class_name": "PrepDeviceStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_unit_test_runner"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs unit test.",
+        "step_factories": [
+            {
+                "class_name": "UnitTestRunStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_unit_test_runner"
+            },
+            {
+                "class_name": "UploadResultToSpongeStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_unit_test_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copy Logs",
+        "step_factories": [
+            {
+                "class_name": "CopyLogsFromRootToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.cloud_logs"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "Updates the good_to_test hint.",
+        "step_factories": [
+            {
+                "class_name": "CreateHintStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.atv_unit_test_runner"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/audio_mirroring.json b/lab_system/master/config/step_factories_recipes/audio_mirroring.json
new file mode 100644
index 0000000..7f3f79a
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/audio_mirroring.json
@@ -0,0 +1,96 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "RecordingPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Copying Build to test slave and installing on device",
+        "step_factories": [
+            {
+                "class_name": "RsyncRepositoriesStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_test_repo"
+            },
+            {
+                "class_name": "RsyncAudioReferenceStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.audio_mirroring_runner"
+            },
+            {
+                "class_name": "RsyncRepositoriesStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.audio_mirroring_runner"
+            },
+            {
+                "class_name": "DefaultOtaFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.fetcher"
+            },
+            {
+                "class_name": "CopyBuildToSlaveStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "InstallBuildStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Sets up logging",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs Audio Mirroring Test",
+        "step_factories": [
+            {
+                "class_name": "AudioMirroringStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copies local log files to Google Storage",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "Processes the output of the test",
+        "step_factories": [
+            {
+                "class_name": "GetLogcatStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.audio_mirroring_runner"
+            },
+            {
+                "class_name": "CopyLogsToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.audio_mirroring_runner"
+            }
+        ],
+        "step_identifier": "output_processing"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/audiodefault_unit_test_recipe.json b/lab_system/master/config/step_factories_recipes/audiodefault_unit_test_recipe.json
new file mode 100644
index 0000000..7fe8127
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/audiodefault_unit_test_recipe.json
@@ -0,0 +1,110 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "UnitTestTmpSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "ReducedLeaseDeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            },
+            {
+                "class_name": "GetFirstDeviceIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.allocator"
+            },
+            {
+                "class_name": "UnitTestPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "BuildFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "BuildInstallerWithoutFormatStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Installs test binaries to unit test device",
+        "step_factories": [
+            {
+                "class_name": "PrepDeviceStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs unit test.",
+        "step_factories": [
+            {
+                "class_name": "UnitTestRunStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "UploadResultToSpongeStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Create good build hint",
+        "step_factories": [
+            {
+                "class_name": "CreateHintStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CopyLogsFromRootToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.cloud_logs"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "A placeholder with no-ops to allow custom logs parsing.",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/bringup_ethernet_port.json b/lab_system/master/config/step_factories_recipes/bringup_ethernet_port.json
new file mode 100644
index 0000000..0896500
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/bringup_ethernet_port.json
@@ -0,0 +1,98 @@
+[
+  {
+    "step_description": "Sets up testrun specific properties",
+    "step_identifier": "properties",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.setup.properties",
+        "class_name": "PropertiesSetterStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Allocates devices for the testrun",
+    "step_identifier": "device_allocation",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+        "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.device.bringup_ethernet_port",
+        "class_name": "BringupEthernetPortStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Fetches and installs builds on devices",
+    "step_identifier": "build_handling",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.build.fetcher",
+        "class_name": "DefaultOtaFetcherStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.build.installer_eurlab",
+        "class_name": "BuildInstallerStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "A placeholder with no-ops to allow pre setup of tests.",
+    "step_identifier": "testrun_setup",
+    "step_factories": [
+      {
+        "class_name": "NoopStepFactory",
+        "module_path": "lab_system.master.steps.factory.common.noop"
+      }
+    ]
+  },
+  {
+    "step_description": "Executes the tests using the eurtest runner.",
+    "step_identifier": "test_running",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.run.test_runner",
+        "class_name": "TestRunnerStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Copies local log files to Google Storage",
+    "step_identifier": "logs_copy",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.output.cloud_logs",
+        "class_name": "CopyLogsToRepositoryStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "A placeholder with no-ops to allow custom logs parsing.",
+    "step_identifier": "output_processing",
+    "step_factories": [
+      {
+        "class_name": "NoopStepFactory",
+        "module_path": "lab_system.master.steps.factory.common.noop"
+      }
+    ]
+  },
+  {
+    "step_description": "Cleans up slave and devices after the testrun",
+    "step_identifier": "testrun_cleanup",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.bringup_ethernet_port",
+        "class_name": "SwapDutWithSwitchIpStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.device.cloud_deallocator",
+        "class_name": "DeviceDeallocatorStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+        "class_name": "CleanupWorkdirStepFactory"
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/step_factories_recipes/chrome_sdk_dev_e2e.json b/lab_system/master/config/step_factories_recipes/chrome_sdk_dev_e2e.json
new file mode 100644
index 0000000..ffd790b
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/chrome_sdk_dev_e2e.json
@@ -0,0 +1,115 @@
+[
+  {
+    "step_description": "Allocate laptop and chromecast devices for the test run",
+    "step_identifier": "device_allocation",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+        "class_name": "DeviceAllocatorStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.sender.allocator",
+        "class_name": "SenderAllocatorStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Set up test environment",
+    "step_identifier": "testrun_setup",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.run.chrome_sdk_runner",
+        "class_name": "SetDeviceIpStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.build.copy_test_repo",
+        "class_name": "CopyRepositoriesStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.chrome.download",
+        "class_name": "DownloadChromeStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.chrome.copy",
+        "class_name": "CopyChromeStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.run.chrome_sdk_runner",
+        "class_name": "SetExtensionVersionStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.run.chrome_sdk_runner",
+        "class_name": "UnzipDevExtensionToTMPStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.extension.copy",
+        "class_name": "CopyExtensionStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.run.chrome_sdk_runner",
+        "class_name": "CopyTestFilesStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs_eurlab",
+        "class_name": "StartLoggingStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Executes the tests",
+    "step_identifier": "test_running",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.run.chrome_sdk_runner",
+        "class_name": "TestRunnerStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Replaces logs_copy with a no-op",
+    "step_identifier": "logs_copy",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.common.noop",
+        "class_name": "NoopStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Processes the output of the test",
+    "step_identifier": "output_processing",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs_eurlab",
+        "class_name": "StopLoggingStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs",
+        "class_name": "CopyLogsToRepositoryStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs",
+        "class_name": "UpdateLogMetadataStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Cleanup",
+    "step_identifier": "testrun_cleanup",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.cloud_deallocator",
+        "class_name": "DeviceDeallocatorStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.sender.deallocator",
+        "class_name": "SenderDeallocatorStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+        "class_name": "CleanupWorkdirStepFactory"
+      }
+    ]
+  }
+]
+
diff --git a/lab_system/master/config/step_factories_recipes/chrome_sdk_other_e2e.json b/lab_system/master/config/step_factories_recipes/chrome_sdk_other_e2e.json
new file mode 100644
index 0000000..e7de4ad
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/chrome_sdk_other_e2e.json
@@ -0,0 +1,111 @@
+[
+  {
+    "step_description": "Allocate laptop and chromecast devices for the test run",
+    "step_identifier": "device_allocation",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+        "class_name": "DeviceAllocatorStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.sender.allocator",
+        "class_name": "SenderAllocatorStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Set up test environment",
+    "step_identifier": "testrun_setup",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.run.chrome_sdk_runner",
+        "class_name": "SetDeviceIpStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.build.copy_test_repo",
+        "class_name": "CopyRepositoriesStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.chrome.download",
+        "class_name": "DownloadChromeStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.chrome.copy",
+        "class_name": "CopyChromeStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.extension.download_cws_extension",
+        "class_name": "DownloadWebStoreExtensionStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.extension.copy",
+        "class_name": "CopyExtensionStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.run.chrome_sdk_runner",
+        "class_name": "CopyTestFilesStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs_eurlab",
+        "class_name": "StartLoggingStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Executes the tests",
+    "step_identifier": "test_running",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.run.chrome_sdk_runner",
+        "class_name": "TestRunnerStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Replaces logs_copy with a no-op",
+    "step_identifier": "logs_copy",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.common.noop",
+        "class_name": "NoopStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Processes the output of the test",
+    "step_identifier": "output_processing",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs_eurlab",
+        "class_name": "StopLoggingStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs",
+        "class_name": "CopyLogsToRepositoryStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs",
+        "class_name": "UpdateLogMetadataStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Cleanup",
+    "step_identifier": "testrun_cleanup",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.cloud_deallocator",
+        "class_name": "DeviceDeallocatorStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.sender.deallocator",
+        "class_name": "SenderDeallocatorStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+        "class_name": "CleanupWorkdirStepFactory"
+      }
+    ]
+  }
+]
+
diff --git a/lab_system/master/config/step_factories_recipes/chrome_sdk_test_mrp_e2e.json b/lab_system/master/config/step_factories_recipes/chrome_sdk_test_mrp_e2e.json
new file mode 100644
index 0000000..3c77f7d
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/chrome_sdk_test_mrp_e2e.json
@@ -0,0 +1,103 @@
+[
+  {
+    "step_description": "Allocate laptop and chromecast devices for the test run",
+    "step_identifier": "device_allocation",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+        "class_name": "DeviceAllocatorStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.sender.allocator",
+        "class_name": "SenderAllocatorStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Set up test environment",
+    "step_identifier": "testrun_setup",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.run.chrome_sdk_runner",
+        "class_name": "SetDeviceIpStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.build.copy_test_repo",
+        "class_name": "CopyRepositoriesStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.chrome.download",
+        "class_name": "DownloadChromeStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.chrome.copy",
+        "class_name": "CopyChromeStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.run.chrome_sdk_runner",
+        "class_name": "CopyTestFilesStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs_eurlab",
+        "class_name": "StartLoggingStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Executes the tests",
+    "step_identifier": "test_running",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.run.chrome_sdk_runner",
+        "class_name": "TestRunnerStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Replaces logs_copy with a no-op",
+    "step_identifier": "logs_copy",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.common.noop",
+        "class_name": "NoopStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Processes the output of the test",
+    "step_identifier": "output_processing",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs_eurlab",
+        "class_name": "StopLoggingStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs",
+        "class_name": "CopyLogsToRepositoryStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.output.logs",
+        "class_name": "UpdateLogMetadataStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Cleanup",
+    "step_identifier": "testrun_cleanup",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.cloud_deallocator",
+        "class_name": "DeviceDeallocatorStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.sender.deallocator",
+        "class_name": "SenderDeallocatorStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+        "class_name": "CleanupWorkdirStepFactory"
+      }
+    ]
+  }
+]
+
diff --git a/lab_system/master/config/step_factories_recipes/claim_hint_unit_test_recipe.json b/lab_system/master/config/step_factories_recipes/claim_hint_unit_test_recipe.json
new file mode 100644
index 0000000..889089e
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/claim_hint_unit_test_recipe.json
@@ -0,0 +1,115 @@
+
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "UnitTestTmpSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "ReducedLeaseDeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            },
+            {
+                "class_name": "GetFirstDeviceIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.allocator"
+            },
+            {
+                "class_name": "UnitTestPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "BuildFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "BuildInstallerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Installs test binaries to unit test device",
+        "step_factories": [
+            {
+                "class_name": "PrepDeviceStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs unit test.",
+        "step_factories": [
+            {
+                "class_name": "UnitTestRunStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "UploadResultToSpongeStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Create good build hint",
+        "step_factories": [
+            {
+                "class_name": "CreateHintStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CopyLogStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CopyLogsFromRootToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.cloud_logs"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "A placeholder with no-ops to allow custom logs parsing.",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/default_with_new_device_allocation.json b/lab_system/master/config/step_factories_recipes/default_with_new_device_allocation.json
new file mode 100644
index 0000000..ab72455
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/default_with_new_device_allocation.json
@@ -0,0 +1,90 @@
+[
+  {
+    "step_description": "Sets up testrun specific properties",
+    "step_identifier": "properties",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.setup.properties",
+        "class_name": "PropertiesSetterStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Allocates devices for the testrun",
+    "step_identifier": "device_allocation",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+        "class_name": "DeviceAllocatorStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Fetches and installs builds on devices",
+    "step_identifier": "build_handling",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.build.fetcher",
+        "class_name": "DefaultOtaFetcherStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.build.installer_eurlab",
+        "class_name": "BuildInstallerStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "A placeholder with no-ops to allow pre setup of tests.",
+    "step_identifier": "testrun_setup",
+    "step_factories": [
+      {
+        "class_name": "NoopStepFactory",
+        "module_path": "lab_system.master.steps.factory.common.noop"
+      }
+    ]
+  },
+  {
+    "step_description": "Executes the tests using the eurtest runner.",
+    "step_identifier": "test_running",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.run.test_runner",
+        "class_name": "TestRunnerStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Copies local log files to Google Storage",
+    "step_identifier": "logs_copy",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.output.cloud_logs",
+        "class_name": "CopyLogsFromRootToRepositoryStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "A placeholder with no-ops to allow custom logs parsing.",
+    "step_identifier": "output_processing",
+    "step_factories": [
+      {
+        "class_name": "NoopStepFactory",
+        "module_path": "lab_system.master.steps.factory.common.noop"
+      }
+    ]
+  },
+  {
+    "step_description": "Cleans up slave and devices after the testrun",
+    "step_identifier": "testrun_cleanup",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.cloud_deallocator",
+        "class_name": "DeviceDeallocatorStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+        "class_name": "CleanupWorkdirStepFactory"
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/step_factories_recipes/fct_test_recipe.json b/lab_system/master/config/step_factories_recipes/fct_test_recipe.json
new file mode 100644
index 0000000..16e1711
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/fct_test_recipe.json
@@ -0,0 +1,90 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "PropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.setup.properties"
+            },
+            {
+                "class_name": "AddUnitTestMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            },
+            {
+                "class_name": "PrepFctWorkDirectoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.fct_test_runner"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "FctBuildFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.fct_test_runner"
+            },
+            {
+                "class_name": "CopyFctBuildToSlaveStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.fct_test_runner"
+            },
+            {
+                "class_name": "InstallFctBuildToDeviceStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.fct_test_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Run factory tests using the test runner.",
+        "step_factories": [
+            {
+                "class_name": "TestRunnerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.test_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copies local log files to Google Storage",
+        "step_factories": [
+            {
+                "class_name": "CopyLogsToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.cloud_logs"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "CleanFctWorkDirectoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.fct_test_runner"
+            },
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "RemoveUnitTestMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/gating_tests_recipe.json b/lab_system/master/config/step_factories_recipes/gating_tests_recipe.json
new file mode 100644
index 0000000..f854763
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/gating_tests_recipe.json
@@ -0,0 +1,98 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "PropertiesSetterStepFactoryWithDummyBuild",
+                "module_path": "lab_system.master.steps.factory.setup.properties"
+            },
+            {
+                "class_name": "AddUnitTestMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "DefaultOtaFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.fetcher"
+            },
+            {
+                "class_name": "BuildInstallerStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.installer_eurlab"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "A placeholder with no-ops to allow pre setup of tests.",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Executes the tests using the eurtest runner.",
+        "step_factories": [
+            {
+                "class_name": "TestRunnerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.test_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copies local log files to Google Storage",
+        "step_factories": [
+            {
+                "class_name": "CopyLogsToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.cloud_logs"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "A placeholder with no-ops to allow custom logs parsing.",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "RemoveUnitTestMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/multi_device_unit_test_recipe.json b/lab_system/master/config/step_factories_recipes/multi_device_unit_test_recipe.json
new file mode 100644
index 0000000..38846eb
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/multi_device_unit_test_recipe.json
@@ -0,0 +1,114 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "UnitTestTmpSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "ReducedLeaseDeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            },
+            {
+                "class_name": "GetCommaSeparatedDeviceIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.allocator"
+            },
+            {
+                "class_name": "UnitTestPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "BuildFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "BuildInstallerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Installs test binaries to unit test device",
+        "step_factories": [
+            {
+                "class_name": "PrepDeviceStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs unit test.",
+        "step_factories": [
+            {
+                "class_name": "UnitTestRunStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "UploadResultToSpongeStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Create good build hint",
+        "step_factories": [
+            {
+                "class_name": "CreateHintStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CopyLogStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CopyLogsToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.logs"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "A placeholder with no-ops to allow custom logs parsing.",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/ota_installable_test_recipe.json b/lab_system/master/config/step_factories_recipes/ota_installable_test_recipe.json
new file mode 100644
index 0000000..4562f09
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/ota_installable_test_recipe.json
@@ -0,0 +1,108 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "UnitTestTmpSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "UnitTestPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "CheckBuildUsability",
+                "module_path": "lab_system.master.steps.factory.run.ota_installable_runner"
+            },
+            {
+                "class_name": "BestEffortReducedLeaseDeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            },
+            {
+                "class_name": "GetFirstDeviceIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.allocator"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "BuildFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "HealthCheckStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.health_check"
+            },
+            {
+                "class_name": "BuildInstallerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.ota_installable_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Runs ota installable test.",
+        "step_factories": [
+            {
+                "class_name": "TestRunnerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.ota_installable_runner"
+            },
+            {
+                "class_name": "UploadResultToSpongeStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.ota_installable_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Create good build hint",
+        "step_factories": [
+            {
+                "class_name": "CopyLogsToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.ota_installable_runner"
+            },
+            {
+                "class_name": "CreateHintStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "A placeholder with no-ops to allow custom logs parsing.",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DisableMdnsStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/remote_display.json b/lab_system/master/config/step_factories_recipes/remote_display.json
new file mode 100644
index 0000000..7d71030
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/remote_display.json
@@ -0,0 +1,106 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "RecordingPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Copying Build to test slave and installing on device",
+        "step_factories": [
+            {
+                "class_name": "RsyncRepositoriesStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_test_repo"
+            },
+            {
+                "class_name": "DefaultOtaFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.fetcher"
+            },
+            {
+                "class_name": "CopyBuildToSlaveStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "TurnOnSpecificDevice",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "InstallBuildStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Sets up logging",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs Remote Display Test",
+        "step_factories": [
+            {
+                "class_name": "RemoteDisplayStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copies local log files to Google Storage",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "Processes the output of the test",
+        "step_factories": [
+            {
+                "class_name": "GetLogcatStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "CopyLogsToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.deallocator"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/remote_display_ios.json b/lab_system/master/config/step_factories_recipes/remote_display_ios.json
new file mode 100644
index 0000000..e3aff32
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/remote_display_ios.json
@@ -0,0 +1,102 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "RecordingPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "ReducedLeaseDeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Copying Build to test slave and installing on device",
+        "step_factories": [
+            {
+                "class_name": "RsyncRepositoriesStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_test_repo"
+            },
+            {
+                "class_name": "DefaultOtaFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.fetcher"
+            },
+            {
+                "class_name": "CopyBuildToSlaveStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "InstallBuildStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Sets up logging",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs Remote Display Test",
+        "step_factories": [
+            {
+                "class_name": "IosRemoteDisplayStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.ios_remote_display_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copies local log files to Google Storage",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "Processes the output of the test",
+        "step_factories": [
+            {
+                "class_name": "GetLogcatStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "CopyLogsToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/remote_display_ios_latency.json b/lab_system/master/config/step_factories_recipes/remote_display_ios_latency.json
new file mode 100644
index 0000000..493b01d
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/remote_display_ios_latency.json
@@ -0,0 +1,106 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "RecordingPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "ReducedLeaseDeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Copying Build to test slave and installing on device",
+        "step_factories": [
+            {
+                "class_name": "RsyncRepositoriesStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_test_repo"
+            },
+            {
+                "class_name": "DefaultOtaFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.fetcher"
+            },
+            {
+                "class_name": "CopyBuildToSlaveStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "InstallBuildStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Sets up logging",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs Remote Display Test",
+        "step_factories": [
+            {
+                "class_name": "IosLatencyStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.ios_remote_display_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copies local log files to Google Storage",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "Processes the output of the test",
+        "step_factories": [
+            {
+                "class_name": "GetLogcatStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "ProcessLatencyPhotoStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.ios_remote_display_runner"
+            },
+            {
+                "class_name": "CopyLogsToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/remote_display_nexus_player.json b/lab_system/master/config/step_factories_recipes/remote_display_nexus_player.json
new file mode 100644
index 0000000..bd495d1
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/remote_display_nexus_player.json
@@ -0,0 +1,102 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "RecordingPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Copying Build to test slave and installing on device",
+        "step_factories": [
+            {
+                "class_name": "RsyncRepositoriesStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.copy_test_repo"
+            },
+            {
+                "class_name": "DefaultOtaFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.fetcher"
+            },
+            {
+                "class_name": "CopyBuildToSlaveStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "InstallBuildStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_nexus_player_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Sets up logging",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs Remote Display Test",
+        "step_factories": [
+            {
+                "class_name": "RemoteDisplayStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_nexus_player_runner"
+            },
+            {
+              "module_path": "lab_system.master.steps.factory.device.deallocator",
+              "class_name": "DeviceDeallocatorStepFactory"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copies local log files to Google Storage",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "Processes the output of the test",
+        "step_factories": [
+            {
+                "class_name": "GetLogcatStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            },
+            {
+                "class_name": "CopyLogsToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.android_mirroring_runner"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+      "step_description": "Cleans up slave and devices after the testrun",
+      "step_identifier": "testrun_cleanup",
+      "step_factories": [
+        {
+          "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+          "class_name": "CleanupWorkdirStepFactory"
+        }
+      ]
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/swarming_unit_test_recipe.json b/lab_system/master/config/step_factories_recipes/swarming_unit_test_recipe.json
new file mode 100644
index 0000000..7d2b4eb
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/swarming_unit_test_recipe.json
@@ -0,0 +1,126 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "UnitTestTmpSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "ReducedLeaseDeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            },
+            {
+                "class_name": "GetCommaSeparatedDeviceIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.allocator"
+            },
+            {
+                "class_name": "UnitTestPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Installs test binaries to unit test device",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs unit test.",
+        "step_factories": [
+            {
+                "class_name": "CreateIsolateStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "ArchiveIsolateStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "GetHashStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "TriggerTestStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CollectTestResultStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "RecipeUploadResultToSpongeStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Create good build hint",
+        "step_factories": [
+            {
+                "class_name": "CreateHintStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "RemoveUnitTestBinariesStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CopyLogsToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.cloud_logs"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "A placeholder with no-ops to allow custom logs parsing.",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/tdls_test.json b/lab_system/master/config/step_factories_recipes/tdls_test.json
new file mode 100644
index 0000000..f3aca50
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/tdls_test.json
@@ -0,0 +1,22 @@
+[
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "StrictDeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Running TDLS test.",
+        "step_factories": [
+            {
+                "class_name": "TdlsTestRunnerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.tdls_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/unit_test_lower_case_product_name_recipe.json b/lab_system/master/config/step_factories_recipes/unit_test_lower_case_product_name_recipe.json
new file mode 100644
index 0000000..1bf4ebf
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/unit_test_lower_case_product_name_recipe.json
@@ -0,0 +1,114 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "UnitTestTmpSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "LowerCaseDeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "GetFirstDeviceIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.allocator"
+            },
+            {
+                "class_name": "UnitTestPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "BuildFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "BuildInstallerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Installs test binaries to unit test device",
+        "step_factories": [
+            {
+                "class_name": "PrepDeviceStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs unit test.",
+        "step_factories": [
+            {
+                "class_name": "UnitTestRunStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "UploadResultToSpongeStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Create good build hint",
+        "step_factories": [
+            {
+                "class_name": "CreateHintStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CopyLogStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CopyLogsFromRootToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.cloud_logs"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "A placeholder with no-ops to allow custom logs parsing.",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/unit_test_no_usb_check_recipe.json b/lab_system/master/config/step_factories_recipes/unit_test_no_usb_check_recipe.json
new file mode 100644
index 0000000..8eabf8d
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/unit_test_no_usb_check_recipe.json
@@ -0,0 +1,114 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "UnitTestTmpSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "ReducedLeaseDeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            },
+            {
+                "class_name": "GetFirstDeviceIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.allocator"
+            },
+            {
+                "class_name": "UnitTestPropertiesSetterStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "BuildFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "BuildInstallerNoUsbCheckStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Installs test binaries to unit test device",
+        "step_factories": [
+            {
+                "class_name": "PrepDeviceStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs unit test.",
+        "step_factories": [
+            {
+                "class_name": "UnitTestRunStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "UploadResultToSpongeStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Create good build hint",
+        "step_factories": [
+            {
+                "class_name": "CreateHintStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CopyLogStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CopyLogsFromRootToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.cloud_logs"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "A placeholder with no-ops to allow custom logs parsing.",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/step_factories_recipes/unit_test_recipe.json b/lab_system/master/config/step_factories_recipes/unit_test_recipe.json
new file mode 100644
index 0000000..a827a8d
--- /dev/null
+++ b/lab_system/master/config/step_factories_recipes/unit_test_recipe.json
@@ -0,0 +1,114 @@
+[
+    {
+        "step_description": "Sets up testrun specific properties",
+        "step_factories": [
+            {
+                "class_name": "PropertiesSetterStepFactoryWithDummyBuild",
+                "module_path": "lab_system.master.steps.factory.setup.properties"
+            },
+            {
+                "class_name": "AddUnitTestMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "properties"
+    },
+    {
+        "step_description": "Allocates devices for the testrun",
+        "step_factories": [
+            {
+                "class_name": "ReducedLeaseDeviceAllocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+            },
+            {
+                "class_name": "GetFirstDeviceIpAddrStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.allocator"
+            }
+        ],
+        "step_identifier": "device_allocation"
+    },
+    {
+        "step_description": "Fetches and installs builds on devices",
+        "step_factories": [
+            {
+                "class_name": "DefaultOtaFetcherStepFactory",
+                "module_path": "lab_system.master.steps.factory.build.fetcher"
+            },
+            {
+                "class_name": "BuildInstallerRollbackStartedMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "build_handling"
+    },
+    {
+        "step_description": "Installs test binaries to unit test device",
+        "step_factories": [
+            {
+                "class_name": "PrepDeviceRollbackStartedMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "testrun_setup"
+    },
+    {
+        "step_description": "Runs unit test.",
+        "step_factories": [
+            {
+                "class_name": "UnitTestRunStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "UploadResultToSpongeRollbackStartedMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            }
+        ],
+        "step_identifier": "test_running"
+    },
+    {
+        "step_description": "Copy log files",
+        "step_factories": [
+            {
+                "class_name": "CopyLogStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CopyLogsFromRootToRepositoryStepFactory",
+                "module_path": "lab_system.master.steps.factory.output.cloud_logs"
+            }
+        ],
+        "step_identifier": "logs_copy"
+    },
+    {
+        "step_description": "A placeholder with no-ops to allow custom logs parsing.",
+        "step_factories": [
+            {
+                "class_name": "NoopStepFactory",
+                "module_path": "lab_system.master.steps.factory.common.noop"
+            }
+        ],
+        "step_identifier": "output_processing"
+    },
+    {
+        "step_description": "Cleans up slave and devices after the testrun",
+        "step_factories": [
+            {
+                "class_name": "DisableMdnsStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "DeviceDeallocatorStepFactory",
+                "module_path": "lab_system.master.steps.factory.device.cloud_deallocator"
+            },
+            {
+                "class_name": "RemoveUnitTestMarkerStepFactory",
+                "module_path": "lab_system.master.steps.factory.run.unit_test_runner"
+            },
+            {
+                "class_name": "CleanupWorkdirStepFactory",
+                "module_path": "lab_system.master.steps.factory.cleanup.workdir"
+            }
+        ],
+        "step_identifier": "testrun_cleanup"
+    }
+]
diff --git a/lab_system/master/config/steps/default.cfg b/lab_system/master/config/steps/default.cfg
new file mode 100644
index 0000000..5248503
--- /dev/null
+++ b/lab_system/master/config/steps/default.cfg
@@ -0,0 +1,102 @@
+[
+  {
+    "step_description": "Sets up testrun specific properties",
+    "step_identifier": "properties",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.setup.properties",
+        "class_name": "PropertiesSetterStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Allocates devices for the testrun",
+    "step_identifier": "device_allocation",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+        "class_name": "DeviceAllocatorStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Fetches and installs builds on devices",
+    "step_identifier": "build_handling",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.device_clean",
+        "class_name": "DeviceCleanStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.build.fetcher",
+	"class_name": "DefaultOtaFetcherStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.device.health_check",
+        "class_name": "HealthCheckStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.build.installer_eurlab",
+        "class_name": "BuildInstallerStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "A placeholder with no-ops to allow pre setup of tests.",
+    "step_identifier": "testrun_setup",
+    "step_factories": [
+      {
+        "class_name": "NoopStepFactory",
+        "module_path": "lab_system.master.steps.factory.common.noop"
+      }
+    ]
+  },
+  {
+    "step_description": "Executes the tests using the eurtest runner.",
+    "step_identifier": "test_running",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.run.test_runner",
+        "class_name": "TestRunnerStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "Copies local log files to Google Storage",
+    "step_identifier": "logs_copy",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.output.cloud_logs",
+        "class_name": "CopyLogsToRepositoryStepFactory"
+      }
+    ]
+  },
+  {
+    "step_description": "A placeholder with no-ops to allow custom logs parsing.",
+    "step_identifier": "output_processing",
+    "step_factories": [
+      {
+        "class_name": "NoopStepFactory",
+        "module_path": "lab_system.master.steps.factory.common.noop"
+      }
+    ]
+  },
+  {
+    "step_description": "Cleans up slave and devices after the testrun",
+    "step_identifier": "testrun_cleanup",
+    "step_factories": [
+      {
+        "module_path": "lab_system.master.steps.factory.device.cloud_deallocator",
+        "class_name": "DisableMdnsStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.device.cloud_deallocator",
+        "class_name": "DeviceDeallocatorStepFactory"
+      },
+      {
+        "module_path": "lab_system.master.steps.factory.cleanup.workdir",
+        "class_name": "CleanupWorkdirStepFactory"
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/swarming/README.md b/lab_system/master/config/swarming/README.md
new file mode 100644
index 0000000..e28366d
--- /dev/null
+++ b/lab_system/master/config/swarming/README.md
@@ -0,0 +1,8 @@
+The bot_config file is used for the swarming bot config.
+
+This version saves the changes made specifically for the Cast team.
+
+To deploy, remove this file to bot_config.py and upload to the
+[Bot Config](https://cast-swarming.appspot.com/restricted/upload/bot_config).
+
+Note: this file is not compliant with the gpylint code style.
diff --git a/lab_system/master/config/swarming/bot_config.py_ b/lab_system/master/config/swarming/bot_config.py_
new file mode 100644
index 0000000..4041c05
--- /dev/null
+++ b/lab_system/master/config/swarming/bot_config.py_
@@ -0,0 +1,248 @@
+# coding: utf-8
+# Copyright 2013 The LUCI Authors. All rights reserved.
+# Use of this source code is governed under the Apache License, Version 2.0
+# that can be found in the LICENSE file.
+
+"""This file is meant to be overriden by the server's specific copy.
+
+You can upload a new version via /restricted/upload/bot_config.
+
+There's 3 types of functions in this file:
+  - get_*() to return properties to describe this bot.
+  - on_*() as hooks based on events happening on the bot.
+  - setup_*() to setup global state on the host.
+
+This file shouldn't import from other scripts in this directory except
+os_utilities which is guaranteed to be usable as an API. It's fine to import
+from stdlib.
+
+Set the environment variable SWARMING_LOAD_TEST=1 to disable the use of
+server-provided bot_config.py. This permits safe load testing.
+
+This file contains unicode to confirm UTF-8 encoded file is well supported.
+Here's a pile of poo: 💩
+"""
+
+import os
+import time
+
+from api import os_utilities
+from api import platforms
+
+# Unused argument 'bot' - pylint: disable=W0613
+
+
+def get_dimensions(bot):
+  # pylint: disable=line-too-long
+  """Returns dict with the bot's dimensions.
+
+  The dimensions are what are used to select the bot that can run each task.
+
+  The bot id will be automatically selected based on the hostname with
+  os_utilities.get_dimensions(). If you want something more special, specify it
+  in your bot_config.py and override the item 'id'.
+
+  The dimensions returned here will be joined with server defined dimensions
+  (extracted from bots.cfg config file based on the bot id). Server defined
+  dimensions override the ones provided by the bot. See bot.Bot.dimensions for
+  more information.
+
+  See https://github.com/luci/luci-py/tree/master/appengine/swarming/doc/Magic-Values.md.
+
+  Arguments:
+  - bot: bot.Bot instance or None. See ../api/bot.py.
+  """
+  dimensions = os_utilities.get_dimensions()
+
+  # The bot base directory is formatted like <HOME>/bots/<Pool>/<Id>
+  id = dimensions['id'][0]
+  index = os.path.basename(bot.base_dir)
+  pool_name = os.path.basename(os.path.dirname(bot.base_dir))
+
+  dimensions['pool'] = [pool_name]
+  dimensions['id'] = ['%s_%s' % (id, index)]
+
+  return dimensions
+
+
+def get_state(bot):
+  # pylint: disable=line-too-long
+  """Returns dict with a state of the bot reported to the server with each poll.
+
+  It is only for dynamic state that changes while bot is running for information
+  for the sysadmins.
+
+  The server can not use this state for immediate scheduling purposes (use
+  'dimensions' for that), but it can use it for maintenance and bookkeeping
+  tasks.
+
+  See https://github.com/luci/luci-py/tree/master/appengine/swarming/doc/Magic-Values.md.
+
+  Arguments:
+  - bot: bot.Bot instance or None. See ../api/bot.py.
+  """
+  return os_utilities.get_state()
+
+
+def get_authentication_headers(bot):
+  """Returns authentication headers and their expiration time.
+
+  The returned headers will be passed with each HTTP request to the Swarming
+  server (and only Swarming server). The bot will use the returned headers until
+  they are close to expiration (usually 6 min, see AUTH_HEADERS_EXPIRATION_SEC
+  in remote_client.py), and then it'll attempt to refresh them by calling
+  get_authentication_headers again.
+
+  Can be used to implement per-bot authentication. If no headers are returned,
+  the server will use only IP whitelist for bot authentication.
+
+  On GCE will use OAuth token of the default GCE service account. It should have
+  "User info" API scope enabled (this can be set when starting an instance). The
+  server should be configured (via bots.cfg) to trust this account (see
+  'require_service_account' in bots.proto).
+
+  May be called by different threads, but never concurrently.
+
+  Arguments:
+  - bot: bot.Bot instance. See ../api/bot.py.
+
+  Returns:
+    Tuple (dict with headers or None, unix timestamp of when they expire).
+  """
+  if platforms.is_gce():
+    tok = platforms.gce.oauth2_access_token()
+    return {'Authorization': 'Bearer %s' % tok}, time.time() + 5*60
+  return (None, None)
+
+
+### Hooks
+
+
+def on_bot_shutdown(bot):
+  """Hook function called when the bot shuts down, usually rebooting.
+
+  It's a good time to do other kinds of cleanup.
+
+  Arguments:
+  - bot: bot.Bot instance. See ../api/bot.py.
+  """
+  pass
+
+
+def on_bot_startup(bot):
+  """Hook function called when the bot starts, before handshake with the server.
+
+  Here the bot may initialize and examine its environment, pick initial state
+  and dimensions to send to the server during the handshake.
+
+  Arguments:
+  - bot: bot.Bot instance. See ../api/bot.py.
+  """
+  pass
+
+
+def on_handshake(bot):
+  """Hook function called when the bot starts, after handshake with the server.
+
+  Here the bot already knows server enforced dimensions (defined in server side
+  bots.cfg file).
+
+  This is called right before starting to poll for tasks. It's a good time to
+  do some final initialization or cleanup that may depend on server provided
+  configuration.
+
+  Arguments:
+  - bot: bot.Bot instance. See ../api/bot.py.
+  """
+  pass
+
+
+def on_before_task(bot, bot_file=None):
+  """Hook function called before running a task.
+
+  It shouldn't do much, since it can't cancel the task so it shouldn't do
+  anything too fancy.
+
+  Arguments:
+  - bot: bot.Bot instance. See ../api/bot.py.
+  - bot_file: Path to file to write information about the state of the bot.
+              This file can be used to pass certain info about the bot
+              to tasks, such as which connected android devices to run on. See
+              https://github.com/luci/luci-py/tree/master/appengine/swarming/doc/Magic-Values.md#run_isolated
+              TODO(bpastene): Remove default value None.
+  """
+  pass
+
+
+def on_after_task(bot, failure, internal_failure, dimensions, summary):
+  """Hook function called after running a task.
+
+  It is an excellent place to do post-task cleanup of temporary files.
+
+  The default implementation restarts after a task failure or an internal
+  failure.
+
+  Arguments:
+  - bot: bot.Bot instance. See ../api/bot.py.
+  - failure: bool, True if the task failed.
+  - internal_failure: bool, True if an internal failure happened.
+  - dimensions: dict, Dimensions requested as part of the task.
+  - summary: dict, Summary of the task execution.
+  """
+  # Example code:
+  #if failure:
+  #  bot.restart('Task failure')
+  #elif internal_failure:
+  #  bot.restart('Internal failure')
+
+
+def on_bot_idle(bot, since_last_action):
+  """Hook function called once when the bot has been idle; when it has no
+  command to execute.
+
+  This is an excellent place to put device in 'cool down' mode or any
+  "pre-warming" kind of stuff that could take several seconds to do, that would
+  not be appropriate to do in on_after_task(). It could be worth waiting for
+  `since_last_action` to be several seconds before doing a more lengthy
+  operation.
+
+  This function is called repeatedly until an action is taken (a task, updating,
+  etc).
+
+  This is a good place to do "auto reboot" for hardware based bots that are
+  rebooted periodically.
+
+  Arguments:
+  - bot: bot.Bot instance. See ../api/bot.py.
+  - since_last_action: time in second since last action; e.g. amount of time the
+                       bot has been idle.
+  """
+  pass
+
+
+### Setup
+
+
+def setup_bot(bot):
+  """Does one time initialization for this bot.
+
+  Returns True if it's fine to start the bot right away. Otherwise, the calling
+  script should exit.
+
+  This is an excellent place to drop a README file in the bot directory, to give
+  more information about the purpose of this bot.
+
+  Example: making this script starts automatically on user login via
+  os_utilities.set_auto_startup_win() or os_utilities.set_auto_startup_osx().
+  """
+  with open(os.path.join(bot.base_dir, 'README'), 'wb') as f:
+    f.write(
+"""This directory contains a Swarming bot.
+
+Swarming source code is hosted at https://github.com/luci/luci-py.
+
+The bot was generated from the server %s. To get the bot's attributes, run:
+
+  python swarming_bot.zip attributes
+""" % bot.server)
+    return True
diff --git a/lab_system/master/config/tests/als_test/als_test.cfg b/lab_system/master/config/tests/als_test/als_test.cfg
new file mode 100644
index 0000000..1b049a4
--- /dev/null
+++ b/lab_system/master/config/tests/als_test/als_test.cfg
@@ -0,0 +1,25 @@
+# Config file for als test
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.als.als_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AlsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.als.als_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AlsTestSequence
+
+# Sign in requirement
+SIGNED_IN=True
+
+# Disable selina sensor in these devices
+DEVICES_WITH_SELINA_SENSOR=elaine
diff --git a/lab_system/master/config/tests/als_test/elaine_input_rgbc_output_ct_and_brightness.yaml b/lab_system/master/config/tests/als_test/elaine_input_rgbc_output_ct_and_brightness.yaml
new file mode 100644
index 0000000..37a1ec6
--- /dev/null
+++ b/lab_system/master/config/tests/als_test/elaine_input_rgbc_output_ct_and_brightness.yaml
@@ -0,0 +1,292 @@
+# RGBC inputs for ambient light sensor, with their expected output color temperature and expected_brightness.
+
+# INPUTS:
+# The RGBC values refer to the amount of light reported by a photodiode sensor.
+# RGB refers to filtered red, green and blue light respectively.
+# C refers to raw, unfiltered 'clear' light, and is roughly the sum of the three separate RGB values.
+
+# OUTPUTS:
+# The expected computed values of color temperature and expected_brightness, given the input RGBC values
+# Color temperature is measured in kelvin and ranges from a 'cool blue' white color, to a 'warm red' white color.
+# Brightness is a percentage value where 100% is the greatest expected_brightness the light sensing system can perceive, while 0% is absolute darkness.
+
+-
+  red: 7555
+  green: 7686
+  blue: 6067
+  clear: 21368
+  expected_color_temp: 6650
+  expected_brightness: 1
+-
+  red: 6980
+  green: 7105
+  blue: 5618
+  clear: 19760
+  expected_color_temp: 6650
+  expected_brightness: 1
+-
+  red: 3002
+  green: 3060
+  blue: 2407
+  clear: 8503
+  expected_color_temp: 6650
+  expected_brightness: 0.532
+-
+  red: 792
+  green: 813
+  blue: 636
+  clear: 2249
+  expected_color_temp: 6650
+  expected_brightness: 0.141
+-
+  red: 235
+  green: 240
+  blue: 188
+  clear: 666
+  expected_color_temp: 6650
+  expected_brightness: 0.033
+-
+  red: 124
+  green: 128
+  blue: 100
+  clear: 353
+  expected_color_temp: 6650
+  expected_brightness: 0.018
+-
+  red: 67
+  green: 69
+  blue: 54
+  clear: 190
+  expected_color_temp: 6650
+  expected_brightness: 0.010
+-
+  red: 0
+  green: 0
+  blue: 0
+  clear: 0
+  expected_color_temp: 2856
+  expected_brightness: 0.001
+-
+  red: 22
+  green: 23
+  blue: 18
+  clear: 63
+  expected_color_temp: 6650
+  expected_brightness: 0.004
+-
+  red: 7956
+  green: 7833
+  blue: 5998
+  clear: 21775
+  expected_color_temp: 6650
+  expected_brightness: 1
+-
+  red: 7087
+  green: 6590
+  blue: 4790
+  clear: 18341
+  expected_color_temp: 6650
+  expected_brightness: 1
+-
+  red: 3003
+  green: 2791
+  blue: 2015
+  clear: 7762
+  expected_color_temp: 6650
+  expected_brightness: 0.488
+-
+  red: 810
+  green: 755
+  blue: 541
+  clear: 2092
+  expected_color_temp: 6650
+  expected_brightness: 0.131
+-
+  red: 232
+  green: 215
+  blue: 154
+  clear: 597
+  expected_color_temp: 6650
+  expected_brightness: 0.037
+-
+  red: 115
+  green: 107
+  blue: 77
+  clear: 297
+  expected_color_temp: 6650
+  expected_brightness: 0.019
+-
+  red: 62
+  green: 58
+  blue: 41
+  clear: 160
+  expected_color_temp: 6650
+  expected_brightness: 0.010
+-
+  red: 23
+  green: 22
+  blue: 15
+  clear: 60
+  expected_color_temp: 6345
+  expected_brightness: 0.005
+-
+  red: 8772
+  green: 7147
+  blue: 4611
+  clear: 20087
+  expected_color_temp: 5241
+  expected_brightness: 1.0
+-
+  red: 7056
+  green: 5742
+  blue: 3705
+  clear: 16150
+  expected_color_temp: 5237
+  expected_brightness: 1
+-
+  red: 2922
+  green: 2382
+  blue: 1521
+  clear: 6684
+  expected_color_temp: 5179
+  expected_brightness: 0.480
+-
+  red: 767
+  green: 625
+  blue: 393
+  clear: 1745
+  expected_color_temp: 5089
+  expected_brightness: 0.127
+-
+  red: 222
+  green: 180
+  blue: 113
+  clear: 503
+  expected_color_temp: 5060
+  expected_brightness: 0.037
+-
+  red: 113
+  green: 92
+  blue: 57
+  clear: 257
+  expected_color_temp: 5006
+  expected_brightness: 0.019
+-
+  red: 55
+  green: 45
+  blue: 28
+  clear: 125
+  expected_color_temp: 5047
+  expected_brightness: 0.009
+-
+  red: 23
+  green: 18
+  blue: 11
+  clear: 52
+  expected_color_temp: 4788
+  expected_brightness: 0.005
+-
+  red: 7830
+  green: 5235
+  blue: 2787
+  clear: 15228
+  expected_color_temp: 3817
+  expected_brightness: 1.0
+-
+  red: 6086
+  green: 4035
+  blue: 2160
+  clear: 11771
+  expected_color_temp: 3801
+  expected_brightness: 1.0
+-
+  red: 2641
+  green: 1741
+  blue: 927
+  clear: 5086
+  expected_color_temp: 3773
+  expected_brightness: 0.538
+-
+  red: 691
+  green: 453
+  blue: 241
+  clear: 1326
+  expected_color_temp: 3753
+  expected_brightness: 0.141
+-
+  red: 188
+  green: 124
+  blue: 66
+  clear: 361
+  expected_color_temp: 3774
+  expected_brightness: 0.038
+-
+  red: 103
+  green: 68
+  blue: 36
+  clear: 198
+  expected_color_temp: 3767
+  expected_brightness: 0.021
+-
+  red: 50
+  green: 33
+  blue: 18
+  clear: 96
+  expected_color_temp: 3822
+  expected_brightness: 0.010
+-
+  red: 24
+  green: 16
+  blue: 8
+  clear: 47
+  expected_color_temp: 3662
+  expected_brightness: 0.006
+-
+  red: 5093
+  green: 2304
+  blue: 1234
+  clear: 7848
+  expected_color_temp: 2856
+  expected_brightness: 0.933
+-
+  red: 4095
+  green: 1751
+  blue: 940
+  clear: 6117
+  expected_color_temp: 2856
+  expected_brightness: 0.719
+-
+  red: 1832
+  green: 774
+  blue: 415
+  clear: 2721
+  expected_color_temp: 2856
+  expected_brightness: 0.319
+-
+  red: 501
+  green: 209
+  blue: 111
+  clear: 739
+  expected_color_temp: 2856
+  expected_brightness: 0.087
+-
+  red: 132
+  green: 55
+  blue: 29
+  clear: 194
+  expected_color_temp: 2856
+  expected_brightness: 0.017
+-
+  red: 72
+  green: 30
+  blue: 16
+  clear: 106
+  expected_color_temp: 2856
+  expected_brightness: 0.010
+-
+  red: 36
+  green: 15
+  blue: 8
+  clear: 52
+  expected_color_temp: 2856
+  expected_brightness: 0.005
diff --git a/lab_system/master/config/tests/als_test/estelle_input_rgbc_output_ct_and_brightness.yaml b/lab_system/master/config/tests/als_test/estelle_input_rgbc_output_ct_and_brightness.yaml
new file mode 100644
index 0000000..cb7503c
--- /dev/null
+++ b/lab_system/master/config/tests/als_test/estelle_input_rgbc_output_ct_and_brightness.yaml
@@ -0,0 +1,292 @@
+# RGBC inputs for ambient light sensor, with their expected output color temperature and expected_brightness.
+
+# INPUTS:
+# The RGBC values refer to the amount of light reported by a photodiode sensor.
+# RGB refers to filtered red, green and blue light respectively.
+# C refers to raw, unfiltered 'clear' light, and is roughly the sum of the three separate RGB values.
+
+# OUTPUTS:
+# The expected computed values of color temperature and expected_brightness, given the input RGBC values
+# Color temperature is measured in kelvin and ranges from a 'cool blue' white color, to a 'warm red' white color.
+# Brightness is a percentage value where 100% is the greatest expected_brightness the light sensing system can perceive, while 0% is absolute darkness.
+
+-
+  red: 7555
+  green: 7686
+  blue: 6067
+  clear: 21368
+  expected_color_temp: 6650
+  expected_brightness: 0.918
+-
+  red: 6980
+  green: 7105
+  blue: 5618
+  clear: 19760
+  expected_color_temp: 6650
+  expected_brightness: 0.849
+-
+  red: 3002
+  green: 3060
+  blue: 2407
+  clear: 8503
+  expected_color_temp: 6650
+  expected_brightness: 0.365
+-
+  red: 792
+  green: 813
+  blue: 636
+  clear: 2249
+  expected_color_temp: 6650
+  expected_brightness: 0.118
+-
+  red: 235
+  green: 240
+  blue: 188
+  clear: 666
+  expected_color_temp: 6650
+  expected_brightness: 0.039
+-
+  red: 124
+  green: 128
+  blue: 100
+  clear: 353
+  expected_color_temp: 6650
+  expected_brightness: 0.021
+-
+  red: 67
+  green: 69
+  blue: 54
+  clear: 190
+  expected_color_temp: 6650
+  expected_brightness: 0.012
+-
+  red: 0
+  green: 0
+  blue: 0
+  clear: 0
+  expected_color_temp: 2856
+  expected_brightness: 0.001
+-
+  red: 22
+  green: 23
+  blue: 18
+  clear: 63
+  expected_color_temp: 6650
+  expected_brightness: 0.005
+-
+  red: 7956
+  green: 7833
+  blue: 5998
+  clear: 21775
+  expected_color_temp: 6650
+  expected_brightness: 0.941
+-
+  red: 7087
+  green: 6590
+  blue: 4790
+  clear: 18341
+  expected_color_temp: 6650
+  expected_brightness: 0.886
+-
+  red: 3003
+  green: 2791
+  blue: 2015
+  clear: 7762
+  expected_color_temp: 6617
+  expected_brightness: 0.381
+-
+  red: 810
+  green: 755
+  blue: 541
+  clear: 2092
+  expected_color_temp: 6515
+  expected_brightness: 0.120
+-
+  red: 232
+  green: 215
+  blue: 154
+  clear: 597
+  expected_color_temp: 6477
+  expected_brightness: 0.037
+-
+  red: 115
+  green: 107
+  blue: 77
+  clear: 297
+  expected_color_temp: 6572
+  expected_brightness: 0.019
+-
+  red: 62
+  green: 58
+  blue: 41
+  clear: 160
+  expected_color_temp: 6333
+  expected_brightness: 0.011
+-
+  red: 23
+  green: 22
+  blue: 15
+  clear: 60
+  expected_color_temp: 5975
+  expected_brightness: 0.005
+-
+  red: 8772
+  green: 7147
+  blue: 4611
+  clear: 20087
+  expected_color_temp: 4676
+  expected_brightness: 1.0
+-
+  red: 7056
+  green: 5742
+  blue: 3705
+  clear: 16150
+  expected_color_temp: 4671
+  expected_brightness: 0.947
+-
+  red: 2922
+  green: 2382
+  blue: 1521
+  clear: 6684
+  expected_color_temp: 4604
+  expected_brightness: 0.395
+-
+  red: 767
+  green: 625
+  blue: 393
+  clear: 1745
+  expected_color_temp: 4497
+  expected_brightness: 0.128
+-
+  red: 222
+  green: 180
+  blue: 113
+  clear: 503
+  expected_color_temp: 4463
+  expected_brightness: 0.042
+-
+  red: 113
+  green: 92
+  blue: 57
+  clear: 257
+  expected_color_temp: 4399
+  expected_brightness: 0.023
+-
+  red: 55
+  green: 45
+  blue: 28
+  clear: 125
+  expected_color_temp: 4448
+  expected_brightness: 0.012
+-
+  red: 23
+  green: 18
+  blue: 11
+  clear: 52
+  expected_color_temp: 4143
+  expected_brightness: 0.006
+-
+  red: 7830
+  green: 5235
+  blue: 2787
+  clear: 15228
+  expected_color_temp: 3001
+  expected_brightness: 0.991
+-
+  red: 6086
+  green: 4035
+  blue: 2160
+  clear: 11771
+  expected_color_temp: 2982
+  expected_brightness: 0.763
+-
+  red: 2641
+  green: 1741
+  blue: 927
+  clear: 5086
+  expected_color_temp: 2948
+  expected_brightness: 0.355
+-
+  red: 691
+  green: 453
+  blue: 241
+  clear: 1326
+  expected_color_temp: 2926
+  expected_brightness: 0.144
+-
+  red: 188
+  green: 124
+  blue: 66
+  clear: 361
+  expected_color_temp: 2950
+  expected_brightness: 0.042
+-
+  red: 103
+  green: 68
+  blue: 36
+  clear: 198
+  expected_color_temp: 2942
+  expected_brightness: 0.024
+-
+  red: 50
+  green: 33
+  blue: 18
+  clear: 96
+  expected_color_temp: 3006
+  expected_brightness: 0.012
+-
+  red: 24
+  green: 16
+  blue: 8
+  clear: 47
+  expected_color_temp: 2868
+  expected_brightness: 0.006
+-
+  red: 5093
+  green: 2304
+  blue: 1234
+  clear: 7848
+  expected_color_temp: 2856
+  expected_brightness: 0.474
+-
+  red: 4095
+  green: 1751
+  blue: 940
+  clear: 6117
+  expected_color_temp: 2856
+  expected_brightness: 0.392
+-
+  red: 1832
+  green: 774
+  blue: 415
+  clear: 2721
+  expected_color_temp: 2856
+  expected_brightness: 0.225
+-
+  red: 501
+  green: 209
+  blue: 111
+  clear: 739
+  expected_color_temp: 2856
+  expected_brightness: 0.078
+-
+  red: 132
+  green: 55
+  blue: 29
+  clear: 194
+  expected_color_temp: 2856
+  expected_brightness: 0.022
+-
+  red: 72
+  green: 30
+  blue: 16
+  clear: 106
+  expected_color_temp: 2856
+  expected_brightness: 0.013
+-
+  red: 36
+  green: 15
+  blue: 8
+  clear: 52
+  expected_color_temp: 2856
+  expected_brightness: 0.007
diff --git a/lab_system/master/config/tests/als_test/newman_input_rgbc_output_ct_and_brightness.yaml b/lab_system/master/config/tests/als_test/newman_input_rgbc_output_ct_and_brightness.yaml
new file mode 100644
index 0000000..a8cca14
--- /dev/null
+++ b/lab_system/master/config/tests/als_test/newman_input_rgbc_output_ct_and_brightness.yaml
@@ -0,0 +1,280 @@
+-
+  red: 7555
+  green: 7686
+  blue: 6067
+  clear: 21368
+  expected_color_temp: 6650
+  expected_brightness: 1.0
+-
+  red: 6980
+  green: 7105
+  blue: 5618
+  clear: 19760
+  expected_color_temp: 6650
+  expected_brightness: 1.0
+-
+  red: 3002
+  green: 3060
+  blue: 2407
+  clear: 8503
+  expected_color_temp: 6650
+  expected_brightness: 0.598
+-
+  red: 792
+  green: 813
+  blue: 636
+  clear: 2249
+  expected_color_temp: 6650
+  expected_brightness: 0.158
+-
+  red: 235
+  green: 240
+  blue: 188
+  clear: 666
+  expected_color_temp: 6650
+  expected_brightness: 0.048
+-
+  red: 124
+  green: 128
+  blue: 100
+  clear: 353
+  expected_color_temp: 6650
+  expected_brightness: 0.026
+-
+  red: 67
+  green: 69
+  blue: 54
+  clear: 190
+  expected_color_temp: 6650
+  expected_brightness: 0.015
+-
+  red: 0
+  green: 0
+  blue: 0
+  clear: 0
+  expected_color_temp: 2856
+  expected_brightness: 0.001
+-
+  red: 22
+  green: 23
+  blue: 18
+  clear: 63
+  expected_color_temp: 6650
+  expected_brightness: 0.006
+-
+  red: 7956
+  green: 7833
+  blue: 5998
+  clear: 21775
+  expected_color_temp: 6650
+  expected_brightness: 1.0
+-
+  red: 7087
+  green: 6590
+  blue: 4790
+  clear: 18341
+  expected_color_temp: 6650
+  expected_brightness: 1.0
+-
+  red: 3003
+  green: 2791
+  blue: 2015
+  clear: 7762
+  expected_color_temp: 6650
+  expected_brightness: 0.555
+-
+  red: 810
+  green: 755
+  blue: 541
+  clear: 2092
+  expected_color_temp: 6650
+  expected_brightness: 0.150
+-
+  red: 232
+  green: 215
+  blue: 154
+  clear: 597
+  expected_color_temp: 6650
+  expected_brightness: 0.044
+-
+  red: 115
+  green: 107
+  blue: 77
+  clear: 297
+  expected_color_temp: 6650
+  expected_brightness: 0.022
+-
+  red: 62
+  green: 58
+  blue: 41
+  clear: 160
+  expected_color_temp: 6649
+  expected_brightness: 0.013
+-
+  red: 23
+  green: 22
+  blue: 15
+  clear: 60
+  expected_color_temp: 6344
+  expected_brightness: 0.006
+-
+  red: 8772
+  green: 7147
+  blue: 4611
+  clear: 20087
+  expected_color_temp: 5240
+  expected_brightness: 1.0
+-
+  red: 7056
+  green: 5742
+  blue: 3705
+  clear: 16150
+  expected_color_temp: 5236
+  expected_brightness: 1.0
+-
+  red: 2922
+  green: 2382
+  blue: 1521
+  clear: 6684
+  expected_color_temp: 5179
+  expected_brightness: 0.550
+-
+  red: 767
+  green: 625
+  blue: 393
+  clear: 1745
+  expected_color_temp: 5088
+  expected_brightness: 0.145
+-
+  red: 222
+  green: 180
+  blue: 113
+  clear: 503
+  expected_color_temp: 5060
+  expected_brightness: 0.043
+-
+  red: 113
+  green: 92
+  blue: 57
+  clear: 257
+  expected_color_temp: 5005
+  expected_brightness: 0.023
+-
+  red: 55
+  green: 45
+  blue: 28
+  clear: 125
+  expected_color_temp: 5046
+  expected_brightness: 0.012
+-
+  red: 23
+  green: 18
+  blue: 11
+  clear: 52
+  expected_color_temp: 4788
+  expected_brightness: 0.006
+-
+  red: 7830
+  green: 5235
+  blue: 2787
+  clear: 15228
+  expected_color_temp: 3817
+  expected_brightness: 1.0
+-
+  red: 6086
+  green: 4035
+  blue: 2160
+  clear: 11771
+  expected_color_temp: 3800
+  expected_brightness: 1.0
+-
+  red: 2641
+  green: 1741
+  blue: 927
+  clear: 5086
+  expected_color_temp: 3772
+  expected_brightness: 0.518
+-
+  red: 691
+  green: 453
+  blue: 241
+  clear: 1326
+  expected_color_temp: 3753
+  expected_brightness: 0.138
+-
+  red: 188
+  green: 124
+  blue: 66
+  clear: 361
+  expected_color_temp: 3773
+  expected_brightness: 0.039
+-
+  red: 103
+  green: 68
+  blue: 36
+  clear: 198
+  expected_color_temp: 3767
+  expected_brightness: 0.022
+-
+  red: 50
+  green: 33
+  blue: 18
+  clear: 96
+  expected_color_temp: 3821
+  expected_brightness: 0.011
+-
+  red: 24
+  green: 16
+  blue: 8
+  clear: 47
+  expected_color_temp: 3662.22
+  expected_brightness: 0.006
+-
+  red: 5093
+  green: 2304
+  blue: 1234
+  clear: 7848
+  expected_color_temp: 2856
+  expected_brightness: 0.836
+-
+  red: 4095
+  green: 1751
+  blue: 940
+  clear: 6117
+  expected_color_temp: 2856
+  expected_brightness: 0.645
+-
+  red: 1832
+  green: 774
+  blue: 415
+  clear: 2721
+  expected_color_temp: 2856
+  expected_brightness: 0.285
+-
+  red: 501
+  green: 209
+  blue: 111
+  clear: 739
+  expected_color_temp: 2856
+  expected_brightness: 0.079
+-
+  red: 132
+  green: 55
+  blue: 29
+  clear: 194
+  expected_color_temp: 2856
+  expected_brightness: 0.021
+-
+  red: 72
+  green: 30
+  blue: 16
+  clear: 106
+  expected_color_temp: 2856
+  expected_brightness: 0.012
+-
+  red: 36
+  green: 15
+  blue: 8
+  clear: 52
+  expected_color_temp: 2856
+  expected_brightness: 0.007
diff --git a/lab_system/master/config/tests/android_mirroring_uma/android_mirroring_uma.cfg b/lab_system/master/config/tests/android_mirroring_uma/android_mirroring_uma.cfg
new file mode 100644
index 0000000..67eb59c
--- /dev/null
+++ b/lab_system/master/config/tests/android_mirroring_uma/android_mirroring_uma.cfg
@@ -0,0 +1,20 @@
+# Config file for the uma logging wifi tests:
+# Those test cases will rely on devices with only wifi connection.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.uma_logging.uma_logging_screen_cast_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=UmaLoggingScreenCastTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.uma_logging.uma_logging_screen_cast_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=UmaLoggingTestSequence
diff --git a/lab_system/master/config/tests/android_mirroring_uma/test_defs.json b/lab_system/master/config/tests/android_mirroring_uma/test_defs.json
new file mode 100644
index 0000000..7a17df8
--- /dev/null
+++ b/lab_system/master/config/tests/android_mirroring_uma/test_defs.json
@@ -0,0 +1,13 @@
+[
+  {
+    "group_name": "android_mirroring_uma",
+    "slave_name": "android_mirroring_uma",
+    "test_name": "android_mirroring_uma",
+    "branch": ["master", "release"],
+    "device_type": "earth",
+    "device_group": "android_mirroring_uma",
+    "identifier": "android_mirroring_uma",
+    "scheduler": "0 */6 * * *",
+    "step_factories_recipe": "step_factories_recipes/android_mirroring_uma.json"
+  }
+]
diff --git a/lab_system/master/config/tests/androidthings_unit_test/test_defs.json b/lab_system/master/config/tests/androidthings_unit_test/test_defs.json
new file mode 100644
index 0000000..6917223
--- /dev/null
+++ b/lab_system/master/config/tests/androidthings_unit_test/test_defs.json
@@ -0,0 +1,16 @@
+[
+    {
+        "branch": ["master", "androidthings_release", "androidthings_next_release"],
+	"build_flavor": "Release",
+        "device_group": "unit_test",
+        "device_type": "imx7d_pico",
+        "group_name": "cast_androidthings",
+        "identifier": "androidthings",
+        "scheduler": "*/10 * * * *",
+        "slave_name": "androidthings",
+        "slave_number": [
+            1
+        ],
+        "step_factories_recipe": "step_factories_recipes/androidthings_unit_test_recipe.json"
+    }
+]
diff --git a/lab_system/master/config/tests/app_prelaunch/app_prelaunch.cfg b/lab_system/master/config/tests/app_prelaunch/app_prelaunch.cfg
new file mode 100644
index 0000000..daee20c
--- /dev/null
+++ b/lab_system/master/config/tests/app_prelaunch/app_prelaunch.cfg
@@ -0,0 +1,21 @@
+# Config file for app prelaunch tests on multiple apps.
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.app_prelaunch.app_prelaunch_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AppPreLaunchTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.app_prelaunch.app_prelaunch_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AppPreLaunchIterableTestSequence
+
+# Name of the file (in the test's configuration directory) containing
+# the apps to test with app prelaunch. The file has the format
+# {'apps': [{'name':<app_name>,'id':<app_id>}...]}.
+APPS_FILE_NAME=apps.json
+
+# App launch state that indicates the app has launched.
+APP_STARTED_STATE_NAME=first_paint
diff --git a/lab_system/master/config/tests/app_prelaunch/app_prelaunch_c4a.cfg b/lab_system/master/config/tests/app_prelaunch/app_prelaunch_c4a.cfg
new file mode 100644
index 0000000..757ff3b
--- /dev/null
+++ b/lab_system/master/config/tests/app_prelaunch/app_prelaunch_c4a.cfg
@@ -0,0 +1,21 @@
+# Config file for app prefetch for C4A devices.
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.app_prelaunch.app_prelaunch_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AppPreLaunchTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.app_prelaunch.app_prelaunch_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AppPreLaunchIterableTestSequence
+
+# Name of the file (in the test's configuration directory) containing
+# the apps to test with app prelaunch. The file has the format
+# {'apps': [{'name':<app_name>,'id':<app_id>}...]}.
+APPS_FILE_NAME=apps_c4a.json
+
+# App launch state that indicates the app has launched.
+APP_STARTED_STATE_NAME=first_audio
diff --git a/lab_system/master/config/tests/app_prelaunch/app_prelaunch_mpl.cfg b/lab_system/master/config/tests/app_prelaunch/app_prelaunch_mpl.cfg
new file mode 100644
index 0000000..271ced7
--- /dev/null
+++ b/lab_system/master/config/tests/app_prelaunch/app_prelaunch_mpl.cfg
@@ -0,0 +1,21 @@
+# Config file for app prelaunch tests on multiple apps.
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.app_prelaunch.app_prelaunch_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AppPreLaunchTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.app_prelaunch.app_prelaunch_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AppPreLaunchIterableTestSequence
+
+# Name of the file (in the test's configuration directory) containing
+# the apps to test with app prelaunch. The file has the format
+# {'apps': [{'name':<app_name>,'id':<app_id>}...]}.
+APPS_FILE_NAME=apps_mpl.json
+
+# App launch state that indicates the app has launched.
+APP_STARTED_STATE_NAME=first_paint
diff --git a/lab_system/master/config/tests/app_prelaunch/apps.json b/lab_system/master/config/tests/app_prelaunch/apps.json
new file mode 100644
index 0000000..f08fc88
--- /dev/null
+++ b/lab_system/master/config/tests/app_prelaunch/apps.json
@@ -0,0 +1,34 @@
+{"apps": [
+    {"id": "233637DE", "name": "YouTube"},
+    {"id": "CA5E8412", "name": "Netflix"},
+    {"id": "16EA8A8F", "name": "Hulu"},
+    {"id": "01CE20A3", "name": "HBO GO"},
+    {"id": "B723160F", "name": "HBO Now"},
+    {"id": "358E83DC", "name": "Twitch"},
+    {"id": "2872939A", "name": "Google Play Music"},
+    {"id": "B0E6A39F", "name": "Viaplay"},
+    {"id": "5E81F6DB", "name": "BBC iPlayer"},
+    {"id": "C193E492", "name": "WatchESPN"},
+    {"id": "C3A34AD9", "name": "dTV"},
+    {"id": "F05C50C9", "name": "Google Play Music"},
+    {"id": "4A7E3D8E", "name": "MLBTV PROD/Akamai, release app"},
+    {"id": "E92AD4B3", "name": "WATCH ABC"},
+    {"id": "AA666EDD", "name": "Crunchyroll"},
+    {"id": "C1132BA1", "name": "Showtime Anytime"},
+    {"id": "0BBDC217", "name": "CBS All Access"},
+    {"id": "091CBEE6", "name": "FOX Now"},
+    {"id": "12F05308", "name": "TuneIn Free"},
+    {"id": "8DD9AEA2", "name": "TuneIn Pro"},
+    {"id": "36061251", "name": "Pandora"},
+    {"id": "EBCB3521", "name": "Rhapsody/Napster"},
+    {"id": "73290360", "name": "Google Play Music CQ"},
+    {"id": "3F70D486", "name": "SVT Play"},
+    {"id": "E874CC4F", "name": "NowTV"},
+    {"id": "D4ECB123", "name": "BT Sport"},
+    {"id": "8F76BAF4", "name": "My Canal"},
+    {"id": "8DE85673", "name": "FranceTV"},
+    {"id": "20E5CA27", "name": "Canal Play"},
+    {"id": "CF54A666", "name": "Maxdome"},
+    {"id": "CD28C71B", "name": "Red Bull TV US"}
+  ]
+}
diff --git a/lab_system/master/config/tests/app_prelaunch/apps_c4a.json b/lab_system/master/config/tests/app_prelaunch/apps_c4a.json
new file mode 100644
index 0000000..12a216d
--- /dev/null
+++ b/lab_system/master/config/tests/app_prelaunch/apps_c4a.json
@@ -0,0 +1,9 @@
+{"apps": [
+    {"id": "12F05308", "name": "TuneIn Free"},
+    {"id": "2872939A", "name": "Play Music"},
+    {"id": "73290360", "name": "Play Music CQ"},
+    {"id": "36061251", "name": "Pandora"},
+    {"id": "8DD9AEA2", "name": "TuneIn Pro"},
+    {"id": "EBCB3521", "name": "Rhapsody/Napster"}
+  ]
+}
diff --git a/lab_system/master/config/tests/app_prelaunch/apps_mpl.json b/lab_system/master/config/tests/app_prelaunch/apps_mpl.json
new file mode 100644
index 0000000..02af99c
--- /dev/null
+++ b/lab_system/master/config/tests/app_prelaunch/apps_mpl.json
@@ -0,0 +1,4 @@
+{"apps": [
+  {"id": "CC1AD845", "name": "Default Receiver"}
+]
+}
diff --git a/lab_system/master/config/tests/app_prelaunch/cast_background_tab.cfg b/lab_system/master/config/tests/app_prelaunch/cast_background_tab.cfg
new file mode 100644
index 0000000..c2636c8
--- /dev/null
+++ b/lab_system/master/config/tests/app_prelaunch/cast_background_tab.cfg
@@ -0,0 +1,16 @@
+# Config file for app prelaunch tests on a single app.
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.app_prelaunch.app_prelaunch_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastBackgroundTabTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.app_prelaunch.app_prelaunch_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AppPreLaunchTestSequence
+
+# App launch state that indicates the app has launched.
+APP_STARTED_STATE_NAME=first_paint
diff --git a/lab_system/master/config/tests/app_prelaunch/test_defs.json b/lab_system/master/config/tests/app_prelaunch/test_defs.json
new file mode 100644
index 0000000..957aa0a
--- /dev/null
+++ b/lab_system/master/config/tests/app_prelaunch/test_defs.json
@@ -0,0 +1,62 @@
+[
+  {
+    "group_name": "app_prelaunch",
+    "slave_name": "app_prelaunch",
+    "test_name": "app_prelaunch",
+    "identifier": "app_prelaunch",
+    "device_type": "eureka",
+    "device_group": "eureka_background_tab_enabled",
+    "hidden": "true",
+    "scheduler": "1 */8 * * *",
+    "branch": ["master", "release", "on_demand"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "app_prelaunch",
+    "slave_name": "app_prelaunch_v2",
+    "test_name": "app_prelaunch",
+    "identifier": "app_prelaunch_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_background_tab_enabled",
+    "hidden": "true",
+    "scheduler": "1 */2 * * *",
+    "branch": ["master", "release", "on_demand"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "app_prelaunch",
+    "slave_name": "app_prelaunch_v3",
+    "test_name": "app_prelaunch",
+    "identifier": "app_prelaunch_v3",
+    "device_type": "earth",
+    "device_group": "earth_background_tab_enabled",
+    "hidden": "true",
+    "scheduler": "1 */4 * * *",
+    "branch": ["master", "release", "on_demand"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "app_prelaunch",
+    "slave_name": "app_prelaunch_audio",
+    "test_name": "app_prelaunch_c4a",
+    "identifier": "app_prelaunch_c4a",
+    "device_type": "hendrix",
+    "device_group": "hendrix_background_tab_enabled",
+    "hidden": "true",
+    "scheduler": "1 */2 * * *",
+    "branch": ["master", "release"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "app_prelaunch",
+    "slave_name": "app_prelaunch",
+    "test_name": "app_prelaunch",
+    "identifier": "app_prelaunch_mpl",
+    "device_type": "eureka",
+    "device_group": "eureka_background_tab_enabled",
+    "scheduler": "1 */8 * * *",
+    "branch": ["master", "release", "next_release"],
+    "build_flavor": "user"
+  }
+
+]
diff --git a/lab_system/master/config/tests/apps_launch_monitor/apps.json b/lab_system/master/config/tests/apps_launch_monitor/apps.json
new file mode 100755
index 0000000..fac11ee
--- /dev/null
+++ b/lab_system/master/config/tests/apps_launch_monitor/apps.json
@@ -0,0 +1,5 @@
+{"apps": [
+    {"name": "Netflix", "id": "CA5E8412"},
+    {"name": "YouTube", "id": "233637DE"}
+  ]
+}
diff --git a/lab_system/master/config/tests/apps_launch_monitor/apps_c4a.json b/lab_system/master/config/tests/apps_launch_monitor/apps_c4a.json
new file mode 100644
index 0000000..448ae1d
--- /dev/null
+++ b/lab_system/master/config/tests/apps_launch_monitor/apps_c4a.json
@@ -0,0 +1,13 @@
+{"apps": [
+    {"id": "12F05308", "name": "TuneIn Free"},
+    {"id": "2872939A", "name": "Google Play Music"},
+    {"id": "28BE5D9A", "name": "Deezer"},
+    {"id": "2DB7CC49", "name": "YouTube Music"},
+    {"id": "36061251", "name": "Pandora"},
+    {"id": "8DD9AEA2", "name": "TuneIn Pro"},
+    {"id": "73290360", "name": "Google Play Music CQ"},
+    {"id": "C54DBC0F", "name": "iHeartRadio"},
+    {"id": "DF630089", "name": "Spotify"},
+    {"id": "EBCB3521", "name": "Rhapsody/Napster"}
+  ]
+}
diff --git a/lab_system/master/config/tests/apps_launch_monitor/apps_launch_monitor.cfg b/lab_system/master/config/tests/apps_launch_monitor/apps_launch_monitor.cfg
new file mode 100644
index 0000000..511736c
--- /dev/null
+++ b/lab_system/master/config/tests/apps_launch_monitor/apps_launch_monitor.cfg
@@ -0,0 +1,30 @@
+# Config file for the canvas render performance tests.  # # PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.apps_launch_monitor.apps_launch_monitor_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AppLaunchMonitorTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Relative path from root of test/ repository to the cast/ repository:
+RELATIVE_PATH_TO_CAST=../cast
+
+# Relative path from root of test/ repository to the telemetry repository:
+RELATIVE_PATH_TO_TELEMETRY=../chromium/src/tools/telemetry
+
+# Relative path (from the config/tests dir) to the file containing
+# the apps to test with the apps launch monitor. The file has the format
+# {'apps': [{'name':<app_name>,'id':<app_id>}...]}.
+APPS_FILE_RELATIVE_PATH=apps_launch_monitor/apps.json
+
+# Set to 'true' to disable the collection of resource loading.
+RESOURCES_COLLECTION_DISABLED=false
+# Continue running on the latest build
+# after all have been tested.
+BUILD_SELECTION_POLICY=backtrack_then_latest
diff --git a/lab_system/master/config/tests/apps_launch_monitor/apps_launch_monitor_audio.cfg b/lab_system/master/config/tests/apps_launch_monitor/apps_launch_monitor_audio.cfg
new file mode 100644
index 0000000..e130997
--- /dev/null
+++ b/lab_system/master/config/tests/apps_launch_monitor/apps_launch_monitor_audio.cfg
@@ -0,0 +1,34 @@
+# Config file for collecting app launch data in audio-only devices.
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.apps_launch_monitor.apps_launch_monitor_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AppLaunchMonitorTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.apps_launch_monitor.apps_launch_monitor_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AppsLaunchMonitorTestSequence
+
+# Relative path from root of test/ repository to the cast/ repository:
+RELATIVE_PATH_TO_CAST=../cast
+
+# Relative path from root of test/ repository to the telemetry repository:
+RELATIVE_PATH_TO_TELEMETRY=../chromium/src/tools/telemetry
+
+# Relative path (from the config/tests dir) to the file containing
+# the apps to test with the apps launch monitor. The file has the format
+# {'apps': [{'name':<app_name>,'id':<app_id>}...]}.
+APPS_FILE_RELATIVE_PATH=apps_launch_monitor/apps_c4a.json
+
+# Disable the collection of resource loading for Hendrix.
+RESOURCES_COLLECTION_DISABLED=true
+# Continue running on the latest build
+# after all have been tested.
+BUILD_SELECTION_POLICY=backtrack_then_latest
+
+# This config is used to determine if cast operations should be allowed even if
+# device is not signed-in using a gaia account.
+ENABLE_CAST_WITHOUT_SIGN_IN=True
diff --git a/lab_system/master/config/tests/apps_launch_monitor/test_defs.json b/lab_system/master/config/tests/apps_launch_monitor/test_defs.json
new file mode 100644
index 0000000..846a727
--- /dev/null
+++ b/lab_system/master/config/tests/apps_launch_monitor/test_defs.json
@@ -0,0 +1,79 @@
+[
+  {
+    "group_name": "apps_launch_monitor",
+    "slave_name": "apps_launch_monitor",
+    "test_name": "apps_launch_monitor",
+    "identifier": "apps_launch_monitor",
+    "device_type": "eureka",
+    "device_group": "eureka_debug",
+    "scheduler": "1 0,8,16 * * *",
+    "branch": ["master", "release", "next_release"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "apps_launch_monitor",
+    "slave_name": "apps_launch_monitor_v2",
+    "test_name": "apps_launch_monitor",
+    "identifier": "apps_launch_monitor_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "scheduler": "1 0,4,8,12,16,20 * * *",
+    "branch": ["master", "release", "next_release", "on_demand"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "apps_launch_monitor",
+    "slave_name": "apps_launch_monitor_v3",
+    "test_name": "apps_launch_monitor",
+    "identifier": "apps_launch_monitor_v3",
+    "device_type": "earth",
+    "device_group": "earth_eth",
+    "scheduler": "1 0,4,8,12,16,20 * * *",
+    "branch": ["master", "release", "next_release", "on_demand"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "apps_launch_monitor",
+    "slave_name": "apps_launch_monitor_audio",
+    "test_name": "apps_launch_monitor_audio",
+    "identifier": "apps_launch_monitor_hendrix",
+    "device_type": "hendrix",
+    "device_group": "hendrix_eth",
+    "scheduler": "1 0,4,8,12,16,20 * * *",
+    "branch": ["master", "release", "next_release", "on_demand"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "apps_launch_monitor",
+    "slave_name": "apps_launch_monitor_audio",
+    "test_name": "apps_launch_monitor_audio",
+    "identifier": "apps_launch_monitor_chirp",
+    "device_type": "chirp",
+    "device_group": "chirp",
+    "scheduler": "1 0,2,4,8,12,15,18,20 * * *",
+    "branch": ["master", "release", "next_release"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "apps_launch_monitor",
+    "slave_name": "apps_launch_monitor_luther",
+    "test_name": "apps_launch_monitor",
+    "identifier": "apps_launch_monitor_luther",
+    "device_type": "luther_b2",
+    "device_group": "luther_eth",
+    "scheduler": "1 0,2,4,8,12,15,18,20 * * *",
+    "branch": ["master", "release", "next_release"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "apps_launch_monitor",
+    "slave_name": "apps_launch_monitor_audio",
+    "test_name": "apps_launch_monitor_audio",
+    "identifier": "apps_launch_monitor_joplin",
+    "device_type": "joplin",
+    "device_group": "joplin",
+    "scheduler": "1 0,4,8,12,16,20 * * *",
+    "branch": ["master", "release", "next_release"],
+    "build_flavor": "user"
+  }
+]
diff --git a/lab_system/master/config/tests/assistant_alarm_timer_marketing_queries_test/marketing_golden_queries_with_alarms_media_test.cfg b/lab_system/master/config/tests/assistant_alarm_timer_marketing_queries_test/marketing_golden_queries_with_alarms_media_test.cfg
new file mode 100644
index 0000000..7f7492c
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_alarm_timer_marketing_queries_test/marketing_golden_queries_with_alarms_media_test.cfg
@@ -0,0 +1,46 @@
+# Config file for assistant marketing queries tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_query_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantQueryTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_marketing_golden/media_news_timers
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=marketing_golden_queries_media_news_timers_tests
+
+
+# Number of times a test has to be repeated. This should be an integer.
+# Test will be run these many times and for all successful outcomes the latency
+# numbers will be persisted.
+TEST_ITERATION_COUNT=1
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+# Never backtrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
+# Profilers to Run
+PERF_PROFILERS=cgroup_profiler,spm_profiler
diff --git a/lab_system/master/config/tests/assistant_art_test/assistant_art_samples_test.cfg b/lab_system/master/config/tests/assistant_art_test/assistant_art_samples_test.cfg
new file mode 100644
index 0000000..f87a830
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_art_test/assistant_art_samples_test.cfg
@@ -0,0 +1,50 @@
+ # Config file for follow-up query tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.art_tests.assistant_art_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantArtTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.art_tests.assistant_art_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantArtTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_FILES_PATH=assistant/art_samples/art_samples_1
+DOWNLOAD_DIRECTORY=/tmp/pi/art_samples/art_samples_1
+#  assistant/art_samples/samples_1
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+BUILD_SELECTION_POLICY=backtrack_then_latest
+
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
+# Test Volumes
+TEST_VOLUME=90
+
+# Options are 'Jabberjay', 'ConductedAudio', and 'Chatterbox'
+STIMULUS_TYPE=Chatterbox
+
+#CGROUP_PARAMETER=-c -detailed-memory-report
+
+# Profilers to Run
+PERF_PROFILERS=cgroup_profiler,data_profiler,spm_profiler
diff --git a/lab_system/master/config/tests/assistant_base_test/assistant_idle_device_test.cfg b/lab_system/master/config/tests/assistant_base_test/assistant_idle_device_test.cfg
new file mode 100644
index 0000000..6eb2058
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_base_test/assistant_idle_device_test.cfg
@@ -0,0 +1,36 @@
+# Config file for follow-up query tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=False
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.base_tests.assistant_idle_device_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantIdleDeviceTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantTestSequence
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+BUILD_SELECTION_POLICY=backtrack_then_latest
+
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
+# Profilers to Run
+PERF_PROFILERS=cgroup_profiler,spm_profiler
diff --git a/lab_system/master/config/tests/assistant_base_test/assistant_idle_signed_in_device_test.cfg b/lab_system/master/config/tests/assistant_base_test/assistant_idle_signed_in_device_test.cfg
new file mode 100644
index 0000000..0764eef
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_base_test/assistant_idle_signed_in_device_test.cfg
@@ -0,0 +1,36 @@
+# Config file for follow-up query tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.base_tests.assistant_idle_device_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantIdleDeviceTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantTestSequence
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+BUILD_SELECTION_POLICY=backtrack_then_latest
+
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
+# Profilers to Run
+PERF_PROFILERS=cgroup_profiler,spm_profiler
diff --git a/lab_system/master/config/tests/assistant_duo_voice_calls_basic_test/assistant_duo_voice_calls_basic_test.cfg b/lab_system/master/config/tests/assistant_duo_voice_calls_basic_test/assistant_duo_voice_calls_basic_test.cfg
new file mode 100644
index 0000000..0a1b36c
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_duo_voice_calls_basic_test/assistant_duo_voice_calls_basic_test.cfg
@@ -0,0 +1,29 @@
+# Config file for assistant phone calls tests
+
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_duo_voice_calls_basic_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantDuoVoiceCallsBasicTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/duo_audio_calls
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
diff --git a/lab_system/master/config/tests/assistant_duo_voice_calls_test/assistant_duo_voice_calls_test.cfg b/lab_system/master/config/tests/assistant_duo_voice_calls_test/assistant_duo_voice_calls_test.cfg
new file mode 100644
index 0000000..e45e405
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_duo_voice_calls_test/assistant_duo_voice_calls_test.cfg
@@ -0,0 +1,29 @@
+# Config file for assistant phone calls tests
+
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_duo_voice_calls_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantDuoVoiceCallsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/duo_audio_calls
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
diff --git a/lab_system/master/config/tests/assistant_follow_up_query_test/assistant_follow_up_query_test.cfg b/lab_system/master/config/tests/assistant_follow_up_query_test/assistant_follow_up_query_test.cfg
new file mode 100644
index 0000000..ff45f13
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_follow_up_query_test/assistant_follow_up_query_test.cfg
@@ -0,0 +1,37 @@
+# Config file for follow-up query tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_follow_up_query_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantFollowUpQueryTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_marketing_golden/follow_up/answers
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+BUILD_SELECTION_POLICY=backtrack_then_latest
+
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
+# Profilers to Run
+PERF_PROFILERS=cgroup_profiler,spm_profiler
diff --git a/lab_system/master/config/tests/assistant_ganache_test/assistant_ganache_timer_dialogue_test_dogfood.cfg b/lab_system/master/config/tests/assistant_ganache_test/assistant_ganache_timer_dialogue_test_dogfood.cfg
new file mode 100644
index 0000000..7c1ad1e
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_ganache_test/assistant_ganache_timer_dialogue_test_dogfood.cfg
@@ -0,0 +1,34 @@
+# Config file for follow-up query tests
+
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_ganache_timer_dialogue_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantGanacheTimerDialogueTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/dialogue/ganache
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+BUILD_SELECTION_POLICY=backtrack_then_latest
+
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
diff --git a/lab_system/master/config/tests/assistant_marketing_queries_test/marketing_golden_queries_test.cfg b/lab_system/master/config/tests/assistant_marketing_queries_test/marketing_golden_queries_test.cfg
new file mode 100644
index 0000000..cb7fc3a
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_marketing_queries_test/marketing_golden_queries_test.cfg
@@ -0,0 +1,39 @@
+# Config file for assistant marketing queries tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_marketing_golden/answers
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=marketing_golden_queries_tests
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+# Never backtrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
+
diff --git a/lab_system/master/config/tests/assistant_media_control_test/assistant_playback_commands.cfg b/lab_system/master/config/tests/assistant_media_control_test/assistant_playback_commands.cfg
new file mode 100644
index 0000000..088befc
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_media_control_test/assistant_playback_commands.cfg
@@ -0,0 +1,20 @@
+# Config file for media playback test cases.
+
+TEST_CASE_MODULE=eurtest.tests.assistant.media_playback_test_case
+TEST_CASE_CLASS=AssistantMediaCommandsTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/manual_speedup/playback_commands
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=media_commands
+ASSISTANT_BACKEND=TEAMFOOD_SERVER
+BUILD_SELECTION_POLICY=backtrack_then_latest
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
+# Profilers to Run
+PERF_PROFILERS=cgroup_profiler,spm_profiler
+
diff --git a/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_gpm_default.cfg b/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_gpm_default.cfg
new file mode 100644
index 0000000..e33d0c8
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_gpm_default.cfg
@@ -0,0 +1,17 @@
+# Config file for media playback test cases.
+
+TEST_CASE_MODULE=eurtest.tests.assistant.media_playback_test_case
+TEST_CASE_CLASS=AssistantMediaPlaybackTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/manual_speedup/gpm_2872939A_default/
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=playback_gpm_default
+ASSISTANT_BACKEND=TEAMFOOD_SERVER
+BUILD_SELECTION_POLICY=backtrack_then_latest
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
diff --git a/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_pandora_default.cfg b/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_pandora_default.cfg
new file mode 100644
index 0000000..d79dee9
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_pandora_default.cfg
@@ -0,0 +1,17 @@
+# Config file for media playback test cases.
+
+TEST_CASE_MODULE=eurtest.tests.assistant.media_playback_test_case
+TEST_CASE_CLASS=AssistantMediaPlaybackTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/manual_speedup/pandora_36061251_default/
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=playback_pandora_default
+ASSISTANT_BACKEND=TEAMFOOD_SERVER
+BUILD_SELECTION_POLICY=backtrack_then_latest
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
diff --git a/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_spotify_default.cfg b/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_spotify_default.cfg
new file mode 100644
index 0000000..58d9886
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_spotify_default.cfg
@@ -0,0 +1,20 @@
+# Config file for media playback test cases.
+
+TEST_CASE_MODULE=eurtest.tests.assistant.media_playback_test_case
+TEST_CASE_CLASS=AssistantMediaPlaybackTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/manual_speedup/spotify_CC32E753_default/
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=playback_spotify_default
+ASSISTANT_BACKEND=TEAMFOOD_SERVER
+BUILD_SELECTION_POLICY=backtrack_then_latest
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
+# Profilers to Run
+PERF_PROFILERS=cgroup_profiler,spm_profiler
+
diff --git a/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_tune_in.cfg b/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_tune_in.cfg
new file mode 100644
index 0000000..aac68aa
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_tune_in.cfg
@@ -0,0 +1,17 @@
+# Config file for media playback test cases.
+
+TEST_CASE_MODULE=eurtest.tests.assistant.media_playback_test_case
+TEST_CASE_CLASS=AssistantMediaPlaybackTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/manual_speedup/tunein_12F05308/
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=playback_tunein
+ASSISTANT_BACKEND=TEAMFOOD_SERVER
+BUILD_SELECTION_POLICY=backtrack_then_latest
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
diff --git a/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_ytm_default.cfg b/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_ytm_default.cfg
new file mode 100644
index 0000000..0c1a6c1
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_media_integration_test/assistant_playback_ytm_default.cfg
@@ -0,0 +1,20 @@
+# Config file for media playback test cases.
+
+TEST_CASE_MODULE=eurtest.tests.assistant.media_playback_test_case
+TEST_CASE_CLASS=AssistantMediaPlaybackTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/manual_speedup/ytm_2DB7CC49_default/
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=playback_ytm_default
+ASSISTANT_BACKEND=TEAMFOOD_SERVER
+BUILD_SELECTION_POLICY=backtrack_then_latest
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
+# Profilers to Run
+PERF_PROFILERS=cgroup_profiler,spm_profiler
+
diff --git a/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_google_play_music.cfg b/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_google_play_music.cfg
new file mode 100644
index 0000000..59bf93b
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_google_play_music.cfg
@@ -0,0 +1,17 @@
+# Config file for media playback latency test cases.
+
+TEST_CASE_MODULE=eurtest.tests.assistant.media_playback_test_case
+TEST_CASE_CLASS=AssistantPlaybackLatencyPreloadingTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/local_media_playback/google_play_music/
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=playback_latency_google_play_music
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_pandora.cfg b/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_pandora.cfg
new file mode 100644
index 0000000..0f8696b
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_pandora.cfg
@@ -0,0 +1,17 @@
+# Config file for media playback latency test cases.
+
+TEST_CASE_MODULE=eurtest.tests.assistant.media_playback_test_case
+TEST_CASE_CLASS=AssistantPlaybackLatencyPreloadingTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/local_media_playback/pandora/
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=playback_latency_pandora
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_spotify.cfg b/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_spotify.cfg
new file mode 100644
index 0000000..c8b16d8
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_spotify.cfg
@@ -0,0 +1,17 @@
+# Config file for media playback latency test cases.
+
+TEST_CASE_MODULE=eurtest.tests.assistant.media_playback_test_case
+TEST_CASE_CLASS=AssistantPlaybackLatencyPreloadingTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/local_media_playback/spotify/
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=playback_latency_spotify
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_tune_in.cfg b/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_tune_in.cfg
new file mode 100644
index 0000000..eafe2ef
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_tune_in.cfg
@@ -0,0 +1,17 @@
+# Config file for media playback latency test cases.
+
+TEST_CASE_MODULE=eurtest.tests.assistant.media_playback_test_case
+TEST_CASE_CLASS=AssistantPlaybackLatencyPreloadingTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/local_media_playback/tune_in/
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=playback_latency_tune_in
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_youtube_music.cfg b/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_youtube_music.cfg
new file mode 100644
index 0000000..5b933b3
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_youtube_music.cfg
@@ -0,0 +1,17 @@
+# Config file for media playback latency test cases.
+
+TEST_CASE_MODULE=eurtest.tests.assistant.media_playback_test_case
+TEST_CASE_CLASS=AssistantPlaybackLatencyPreloadingTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/local_media_playback/youtube_music/
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=playback_latency_youtube_music
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_youtube_music_fuchsia.cfg b/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_youtube_music_fuchsia.cfg
new file mode 100644
index 0000000..bc9f252
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_media_latency/assistant_playback_latency_youtube_music_fuchsia.cfg
@@ -0,0 +1,17 @@
+# Config file for media playback latency test cases.
+
+TEST_CASE_MODULE=eurtest.tests.assistant.media_playback_test_case
+TEST_CASE_CLASS=AssistantPlaybackLatencyPreloadingTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.fuchsia_query_driven_test_sequence
+TEST_SEQUENCE_CLASS=FuchsiaQueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/local_media_playback/youtube_music/
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=playback_latency_youtube_music
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/assistant_multi_device_duo_calls_perf_test/assistant_multi_device_duo_calls_perf_test.cfg b/lab_system/master/config/tests/assistant_multi_device_duo_calls_perf_test/assistant_multi_device_duo_calls_perf_test.cfg
new file mode 100644
index 0000000..e098d22
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_multi_device_duo_calls_perf_test/assistant_multi_device_duo_calls_perf_test.cfg
@@ -0,0 +1,25 @@
+# Config file for assistant phone calls tests
+
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_multi_device_duo_calls_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantMultiDeviceDuoCallsPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantMultiDeviceTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/duo_audio_calls
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
diff --git a/lab_system/master/config/tests/assistant_multi_device_duo_calls_test/assistant_multi_device_duo_calls_test.cfg b/lab_system/master/config/tests/assistant_multi_device_duo_calls_test/assistant_multi_device_duo_calls_test.cfg
new file mode 100644
index 0000000..feca111
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_multi_device_duo_calls_test/assistant_multi_device_duo_calls_test.cfg
@@ -0,0 +1,31 @@
+# Config file for assistant phone calls tests
+
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_multi_device_duo_calls_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantMultiDeviceDuoCallsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantMultiDeviceTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/duo_audio_calls
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+
diff --git a/lab_system/master/config/tests/assistant_oda_base_test/assistant_oda_base_test.cfg b/lab_system/master/config/tests/assistant_oda_base_test/assistant_oda_base_test.cfg
new file mode 100644
index 0000000..d385290
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_oda_base_test/assistant_oda_base_test.cfg
@@ -0,0 +1,24 @@
+# Config file for Word Error Rate (WER) tests.
+#
+# Do not modify property names
+# PROPERTIES
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.oda.assistant_oda_base_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantOdaBaseTestCase
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantOdaBaseTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.oda.assistant_oda_base_test_sequence
+
+
diff --git a/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_i18n.cfg b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_i18n.cfg
new file mode 100644
index 0000000..c94d8da
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_i18n.cfg
@@ -0,0 +1,24 @@
+# Config file for Word Error Rate (WER) tests.
+#
+# Do not modify property names
+# PROPERTIES
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/oda_upl/i18n/samples
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.oda.assistant_oda_smoke_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantOdaSmokeTestCase
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantOdaWerTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.oda.assistant_oda_wer_test_sequence
+
+# Enable Personal Results for Device Assistant Settings during device linking
+# This is applicable only for Display Devices
+ALLOW_PERSONAL_RESULTS=True
\ No newline at end of file
diff --git a/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_media_initiation.cfg b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_media_initiation.cfg
new file mode 100644
index 0000000..8e9375e
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_media_initiation.cfg
@@ -0,0 +1,25 @@
+# Config file for ODA Media Initiation tests.
+#
+# Do not modify property names
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.oda.assistant_oda_media_initiation_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantOdaMediaInitiationTestCase
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantOdaBaseTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.oda.assistant_oda_base_test_sequence
+
+# Use Tunnel where applicable
+USE_TUNNEL=False
+
+# Enable Personal Results for Device Assistant Settings during device linking
+# This is applicable only for Display Devices
+ALLOW_PERSONAL_RESULTS=True
+
+
diff --git a/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl.cfg b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl.cfg
new file mode 100644
index 0000000..53ffe4d
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl.cfg
@@ -0,0 +1,44 @@
+# Config file for marble upl
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# GCS location where the alert tone and flow order json are stored
+ALERT_TONE_FILENAME=alert_tone.wav
+BEHAVIORS_JSON_FILENAME=behaviors.json
+FLOWS_JSON_FILENAME=flows.json
+GCS_PATH=assistant/oda_upl
+
+# GCS location where samples are stored
+SAMPLES_GCS_PATH=assistant/oda_upl/samples
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.oda.assistant_oda_upl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantOdaUplTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.oda.assistant_oda_upl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantOdaUplTestSequence
+
+# Use Tunnel where applicable
+USE_TUNNEL=False
+
+# Profilers to Run
+PERF_PROFILERS=cgroup_profiler,spm_profiler
+
+# CGroup Profiler Options to Run
+CGROUP_PARAMETER=-c -dmr
+
+# Timeout in seconds to wait before first test (5 minutes/300 seconds)
+TIMEOUT_SECONDS_BEFORE_FIRST_TEST=300
+
+# Enable Personal Results for Device Assistant Settings during device linking
+# This is applicable only for Display Devices
+ALLOW_PERSONAL_RESULTS=True
\ No newline at end of file
diff --git a/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_no_profilers.cfg b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_no_profilers.cfg
new file mode 100644
index 0000000..a5acef3
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_no_profilers.cfg
@@ -0,0 +1,42 @@
+# Config file for marble upl
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# GCS location where the alert tone and flow order json are stored
+ALERT_TONE_FILENAME=alert_tone.wav
+BEHAVIORS_JSON_FILENAME=behaviors.json
+FLOWS_JSON_FILENAME=flows.json
+GCS_PATH=assistant/oda_upl
+
+# GCS location where samples are stored
+SAMPLES_GCS_PATH=assistant/oda_upl/samples
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.oda.assistant_oda_upl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantOdaUplTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.oda.assistant_oda_upl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantOdaUplTestSequence
+
+# Use Tunnel where applicable
+USE_TUNNEL=False
+
+# Timeout in seconds to wait before first test (5 minutes/300 seconds)
+TIMEOUT_SECONDS_BEFORE_FIRST_TEST=300
+
+# Enable Personal Results for Device Assistant Settings during device linking
+# This is applicable only for Display Devices
+ALLOW_PERSONAL_RESULTS=True
+
+# SAVE ASR TRACES during tests. Default True to save files in test artifacts.
+# If False, files will not be saved in test artifacts.
+SAVE_ASR_TRACES=False
\ No newline at end of file
diff --git a/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_no_profilers_fuchsia.cfg b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_no_profilers_fuchsia.cfg
new file mode 100644
index 0000000..92f9f4f
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_no_profilers_fuchsia.cfg
@@ -0,0 +1,34 @@
+# Config file for marble upl
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# GCS location where the alert tone and flow order json are stored
+ALERT_TONE_FILENAME=alert_tone.wav
+BEHAVIORS_JSON_FILENAME=behaviors.json
+FLOWS_JSON_FILENAME=flows.json
+GCS_PATH=assistant/oda_upl
+
+# GCS location where samples are stored
+SAMPLES_GCS_PATH=assistant/oda_upl/samples
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.oda.assistant_oda_upl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantOdaUplTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.oda.fuchsia_assistant_oda_upl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaAssistantOdaUplTestSequence
+
+# Use Tunnel where applicable
+USE_TUNNEL=False
+
+# Timeout in seconds to wait before first test (5 minutes/300 seconds)
+TIMEOUT_SECONDS_BEFORE_FIRST_TEST=300
\ No newline at end of file
diff --git a/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_test.cfg b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_test.cfg
new file mode 100644
index 0000000..a50d4fc
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_test.cfg
@@ -0,0 +1,38 @@
+# Config file for marble upl
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# GCS location where the alert tone and flow order json are stored
+ALERT_TONE_FILENAME=alert_tone.wav
+BEHAVIORS_JSON_FILENAME=behaviors_test.json
+FLOWS_JSON_FILENAME=flows_test.json
+GCS_PATH=assistant/oda_upl
+
+# GCS location where samples are stored
+SAMPLES_GCS_PATH=assistant/oda_upl/samples
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.oda.assistant_oda_upl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantOdaUplTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.oda.assistant_oda_upl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantOdaUplTestSequence
+
+# Use Tunnel whether to use a ssh tunnel or not
+USE_TUNNEL=False
+
+# Timeout in seconds to wait before first test (5 minutes/300 seconds)
+TIMEOUT_SECONDS_BEFORE_FIRST_TEST=300
+
+# Enable Personal Results for Device Assistant Settings during device linking
+# This is applicable only for Display Devices
+ALLOW_PERSONAL_RESULTS=True
\ No newline at end of file
diff --git a/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_warmwords.cfg b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_warmwords.cfg
new file mode 100644
index 0000000..784575d
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_warmwords.cfg
@@ -0,0 +1,47 @@
+# Config file for marble upl
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# GCS location where the alert tone and flow order json are stored
+ALERT_TONE_FILENAME=alert_tone.wav
+BEHAVIORS_JSON_FILENAME=behaviors_warmwords.json
+FLOWS_JSON_FILENAME=flows_warmwords.json
+GCS_PATH=assistant/oda_upl
+
+# GCS location where samples are stored
+SAMPLES_GCS_PATH=assistant/oda_upl/samples_warmwords
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.oda.assistant_oda_upl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantOdaUplTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.oda.assistant_oda_upl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantOdaUplTestSequence
+
+# Use Tunnel where applicable
+USE_TUNNEL=False
+
+# FLAG TO ENABLE or DISABLE WARM WORDS ON DEVICE
+ENABLE_WARMWORDS=True
+
+# Profilers to Run
+PERF_PROFILERS=cgroup_profiler,spm_profiler
+
+# CGroup Profiler Options to Run
+CGROUP_PARAMETER=-c -dmr
+
+# Timeout in seconds to wait before first test (15 minutes/900 seconds)
+TIMEOUT_SECONDS_BEFORE_FIRST_TEST=900
+
+# Enable Personal Results for Device Assistant Settings during device linking
+# This is applicable only for Display Devices
+ALLOW_PERSONAL_RESULTS=True
\ No newline at end of file
diff --git a/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_warmwords_long_run.cfg b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_warmwords_long_run.cfg
new file mode 100644
index 0000000..6bcd0fa
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_warmwords_long_run.cfg
@@ -0,0 +1,44 @@
+# Config file for marble upl
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# GCS location where the alert tone and flow order json are stored
+ALERT_TONE_FILENAME=alert_tone.wav
+BEHAVIORS_JSON_FILENAME=behaviors_warmwords.json
+FLOWS_JSON_FILENAME=flows_warmwords.json
+GCS_PATH=assistant/oda_upl
+
+# GCS location where samples are stored
+SAMPLES_GCS_PATH=assistant/oda_upl/samples_warmwords
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.oda.assistant_oda_upl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantOdaUplTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.oda.assistant_oda_upl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantOdaUplTestSequence
+
+# Use Tunnel where applicable
+USE_TUNNEL=False
+
+# FLAG TO ENABLE or DISABLE WARM WORDS ON DEVICE
+ENABLE_WARMWORDS=True
+
+# Profilers to Run
+PERF_PROFILERS=cgroup_profiler,spm_profiler
+
+# Timeout in seconds to wait between each test (15 minutes/900 seconds)
+TIMEOUT_SECONDS_BETWEEN_TEST=900
+
+# Enable Personal Results for Device Assistant Settings during device linking
+# This is applicable only for Display Devices
+ALLOW_PERSONAL_RESULTS=True
\ No newline at end of file
diff --git a/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_warmwords_no_profilers_fuchsia.cfg b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_warmwords_no_profilers_fuchsia.cfg
new file mode 100644
index 0000000..acd3790
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_oda_upl/assistant_oda_upl_warmwords_no_profilers_fuchsia.cfg
@@ -0,0 +1,37 @@
+# Config file for marble upl
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# GCS location where the alert tone and flow order json are stored
+ALERT_TONE_FILENAME=alert_tone.wav
+BEHAVIORS_JSON_FILENAME=behaviors_warmwords.json
+FLOWS_JSON_FILENAME=flows_warmwords.json
+GCS_PATH=assistant/oda_upl
+
+# GCS location where samples are stored
+SAMPLES_GCS_PATH=assistant/oda_upl/samples_warmwords
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.oda.assistant_oda_upl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantOdaUplTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.oda.fuchsia_assistant_oda_upl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaAssistantOdaUplTestSequence
+
+# Use Tunnel where applicable
+USE_TUNNEL=False
+
+# FLAG TO ENABLE or DISABLE WARM WORDS ON DEVICE
+ENABLE_WARMWORDS=True
+
+# Timeout in seconds to wait before first test (5 minutes/300 seconds)
+TIMEOUT_SECONDS_BEFORE_FIRST_TEST=300
\ No newline at end of file
diff --git a/lab_system/master/config/tests/assistant_phone_calls_events_test/assistant_phone_call_events_test.cfg b/lab_system/master/config/tests/assistant_phone_calls_events_test/assistant_phone_call_events_test.cfg
new file mode 100644
index 0000000..f322ee4
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_phone_calls_events_test/assistant_phone_call_events_test.cfg
@@ -0,0 +1,36 @@
+# Config file for assistant phone calls tests
+
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_phone_calls_event_validation_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantPhoneCallsEventValidationTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_calls
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+BUILD_SELECTION_POLICY=backtrack_then_latest
+
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
+
diff --git a/lab_system/master/config/tests/assistant_phone_calls_quality_test/assistant_phone_calls_quality_test.cfg b/lab_system/master/config/tests/assistant_phone_calls_quality_test/assistant_phone_calls_quality_test.cfg
new file mode 100644
index 0000000..3ac40de
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_phone_calls_quality_test/assistant_phone_calls_quality_test.cfg
@@ -0,0 +1,36 @@
+# Config file for assistant phone calls tests
+
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_phone_calls_quality_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantPhoneCallsQualityTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_calls
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+BUILD_SELECTION_POLICY=backtrack_then_latest
+
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
+
diff --git a/lab_system/master/config/tests/assistant_phone_calls_test/assistant_phone_calls_test.cfg b/lab_system/master/config/tests/assistant_phone_calls_test/assistant_phone_calls_test.cfg
new file mode 100644
index 0000000..181bc85
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_phone_calls_test/assistant_phone_calls_test.cfg
@@ -0,0 +1,36 @@
+# Config file for assistant phone calls tests
+
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_phone_calls_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantPhoneCallsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_calls
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+BUILD_SELECTION_POLICY=backtrack_then_latest
+
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
+
diff --git a/lab_system/master/config/tests/assistant_presubmit/assistant_presubmit.cfg b/lab_system/master/config/tests/assistant_presubmit/assistant_presubmit.cfg
new file mode 100644
index 0000000..5734b09
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_presubmit/assistant_presubmit.cfg
@@ -0,0 +1,31 @@
+# Config file for presubmit tests on virtual devices.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.presubmit_hotword_count.presubmit_hotword_count_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantHotwordCountTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.presubmit_hotword_count.presubmit_hotword_count_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=PresubmitTestSequence
+
+# PROPERTIES
+
+# Test suite name of test being run
+SUITE_NAME=AssistantHotwordPresubmitTest
+
+# Timeout for complete test run.
+# Currently, it takes 3.5 hrs including failures.
+SUITE_TIMEOUT_SECS=1200
+
+# Use Tunnel where applicable
+
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
diff --git a/lab_system/master/config/tests/assistant_pti_test/assistant_pti_interruption_test.cfg b/lab_system/master/config/tests/assistant_pti_test/assistant_pti_interruption_test.cfg
new file mode 100644
index 0000000..13a2734
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_pti_test/assistant_pti_interruption_test.cfg
@@ -0,0 +1,30 @@
+# Config file for PTI Interruption Test
+#
+# Do not modify property names
+# PROPERTIES
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/PTI/dialogues_to_stop
+
+# Bucket contains the waves from interruptions
+INTERRUPTION_GCS_BUCKET=assistant/PTI/interruptions
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantPtiInterruptionTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.pti.assistant_pti_interruption_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantPtiInterruptionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.pti.assistant_pti_interruption_test_sequence
+
diff --git a/lab_system/master/config/tests/assistant_pti_test/assistant_pti_overhear_test.cfg b/lab_system/master/config/tests/assistant_pti_test/assistant_pti_overhear_test.cfg
new file mode 100644
index 0000000..fadd767
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_pti_test/assistant_pti_overhear_test.cfg
@@ -0,0 +1,33 @@
+# Config file for PTI overhear test results
+#
+# Do not modify property names
+# PROPERTIES
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+BUILD_SELECTION_POLICY=backtrack_then_latest
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/PTI/overhear
+
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantPtiOverhearTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.pti.assistant_pti_overhear_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantPtiOverhearTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.pti.assistant_pti_overhear_test_sequence
+
+
diff --git a/lab_system/master/config/tests/assistant_pti_test/assistant_pti_user_volume_test.cfg b/lab_system/master/config/tests/assistant_pti_test/assistant_pti_user_volume_test.cfg
new file mode 100644
index 0000000..e35c2bf
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_pti_test/assistant_pti_user_volume_test.cfg
@@ -0,0 +1,51 @@
+# Config file for running USER Volume Test
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+DIALOGUE_FILE=pti_rpi_interruption_dialogues.tar.gz
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/PTI/pti_rpi_samples/pti_rpi_interruption_dialogues
+
+GCS_BUCKET=cast-test-data
+GCS_RPI_SAMPLES_PATH=assistant/PTI/pti_rpi_samples
+
+# Location of wav files on RPI
+RPI_AUDIO_SAMPLES_DIR=/home/pi/pti_rpi_samples
+
+RPI_DOWNLOAD_DIR_PATH=/home/pi/pti_rpi_samples
+
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=PtiInterruptionSample
+
+# Test suite name of test being run
+SUITE_NAME=PtiRPIVolumeTests
+
+# Timeout for complete test run.
+# Currently, it takes 3.5 hrs including failures.
+SUITE_TIMEOUT_SECS=14400
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantPtiSpeakerVolumeTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.pti.assistant_pti_speaker_volume_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantPtiSpeakerVolumeTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.pti.assistant_pti_speaker_volume_test_sequence
+
+# Use Tunnel where applicable
+
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
+WAIT_TIME=10
+
+WAV_FILES_DIR_PATH=/home/pi/pti_rpi_interruption_dialogues
diff --git a/lab_system/master/config/tests/assistant_query_latency/answers_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/answers_latency_dogfood.cfg
new file mode 100644
index 0000000..80e68f6
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/answers_latency_dogfood.cfg
@@ -0,0 +1,15 @@
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/answers
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=answers_latency_tests
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/answers_latency_dogfood_fuchsia.cfg b/lab_system/master/config/tests/assistant_query_latency/answers_latency_dogfood_fuchsia.cfg
new file mode 100644
index 0000000..8cde8ed
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/answers_latency_dogfood_fuchsia.cfg
@@ -0,0 +1,15 @@
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.fuchsia_query_driven_test_sequence
+TEST_SEQUENCE_CLASS=FuchsiaQueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/answers
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=answers_latency_tests
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/answers_latency_prod.cfg b/lab_system/master/config/tests/assistant_query_latency/answers_latency_prod.cfg
new file mode 100644
index 0000000..5ed9b8b
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/answers_latency_prod.cfg
@@ -0,0 +1,20 @@
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/answers
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=answers_latency_tests
+
+ASSISTANT_BACKEND=PROD_SERVER
+
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/calculator_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/calculator_latency_dogfood.cfg
new file mode 100644
index 0000000..1e08e15
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/calculator_latency_dogfood.cfg
@@ -0,0 +1,38 @@
+# Config file for assistant latency tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/calculator
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=calculator_latency_tests
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/canonical_queries_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/canonical_queries_latency_dogfood.cfg
new file mode 100644
index 0000000..ef0ca7c
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/canonical_queries_latency_dogfood.cfg
@@ -0,0 +1,45 @@
+# Config file for assistant canonical queries latency tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+
+# Number of times a test has to be repeated. This should be an integer.
+# Test will be run these many times and for all successful outcomes the latency
+# numbers will be persisted.
+TEST_ITERATION_COUNT=1
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+# Relative path (from the config/tests dir) to the file containing
+# the list of audio test config files
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/canonical_queries
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=canonical_queries_latency_tests
+
+BUILD_SELECTION_POLICY=always_use_latest
+
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/currency_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/currency_latency_dogfood.cfg
new file mode 100644
index 0000000..63fe33e
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/currency_latency_dogfood.cfg
@@ -0,0 +1,39 @@
+# Config file for assistant latency tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/currency
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=currency_latency_tests
+
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/dictionary_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/dictionary_latency_dogfood.cfg
new file mode 100644
index 0000000..8beb2fc
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/dictionary_latency_dogfood.cfg
@@ -0,0 +1,37 @@
+# Config file for assistant latency tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/dictionary
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=dictionary_latency_tests
+
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
diff --git a/lab_system/master/config/tests/assistant_query_latency/finance_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/finance_latency_dogfood.cfg
new file mode 100644
index 0000000..d90aa67
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/finance_latency_dogfood.cfg
@@ -0,0 +1,17 @@
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/finance
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=finance_latency_tests
+
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/local_results_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/local_results_latency_dogfood.cfg
new file mode 100644
index 0000000..aa3a963
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/local_results_latency_dogfood.cfg
@@ -0,0 +1,17 @@
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/local_results
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=local_results_latency_tests
+
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/movie_listings_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/movie_listings_latency_dogfood.cfg
new file mode 100644
index 0000000..27298a5
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/movie_listings_latency_dogfood.cfg
@@ -0,0 +1,39 @@
+# Config file for assistant latency tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/movie_listings
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=movie_listings_latency_tests
+
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/nutrition_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/nutrition_latency_dogfood.cfg
new file mode 100644
index 0000000..eb7e809
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/nutrition_latency_dogfood.cfg
@@ -0,0 +1,15 @@
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/nutrition
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=nutrition_latency_tests
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/persona_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/persona_latency_dogfood.cfg
new file mode 100644
index 0000000..1c7c08b
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/persona_latency_dogfood.cfg
@@ -0,0 +1,38 @@
+# Config file for assistant latency tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/persona
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=persona_latency_tests
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/recipes_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/recipes_latency_dogfood.cfg
new file mode 100644
index 0000000..16b4f61
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/recipes_latency_dogfood.cfg
@@ -0,0 +1,39 @@
+# Config file for assistant latency tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/recipes
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=recipes
+
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/sports_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/sports_latency_dogfood.cfg
new file mode 100644
index 0000000..ff7948e
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/sports_latency_dogfood.cfg
@@ -0,0 +1,15 @@
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/sports
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=sports_latency_tests
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/traffic_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/traffic_latency_dogfood.cfg
new file mode 100644
index 0000000..f89ed6a
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/traffic_latency_dogfood.cfg
@@ -0,0 +1,16 @@
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/traffic
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=traffic_latency_tests
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/translations_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/translations_latency_dogfood.cfg
new file mode 100644
index 0000000..12097b6
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/translations_latency_dogfood.cfg
@@ -0,0 +1,39 @@
+# Config file for assistant latency tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/translations
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=translations_latency_tests
+
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/unit_conversions_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/unit_conversions_latency_dogfood.cfg
new file mode 100644
index 0000000..b3c129b
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/unit_conversions_latency_dogfood.cfg
@@ -0,0 +1,39 @@
+# Config file for assistant latency tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/unit_conversion
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=unit_conversions_latency_tests
+
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
+
diff --git a/lab_system/master/config/tests/assistant_query_latency/weather_latency_dogfood.cfg b/lab_system/master/config/tests/assistant_query_latency/weather_latency_dogfood.cfg
new file mode 100644
index 0000000..27b357b
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_query_latency/weather_latency_dogfood.cfg
@@ -0,0 +1,15 @@
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_latency_test_case
+TEST_CASE_CLASS=QueryDrivenLatencyTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_answers_golden/weather
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=weather_latency_tests
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/assistant_remote_casting/assistant_remote_casting_latency.cfg b/lab_system/master/config/tests/assistant_remote_casting/assistant_remote_casting_latency.cfg
new file mode 100644
index 0000000..58cbac0
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_remote_casting/assistant_remote_casting_latency.cfg
@@ -0,0 +1,17 @@
+# Config file for media playback latency test cases.
+
+TEST_CASE_MODULE=eurtest.tests.assistant.remote_casting_test_case
+TEST_CASE_CLASS=AssistantRemoteCastingLatencyTestCase
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.remote_casting_test_case
+TEST_SEQUENCE_CLASS=RemoteCastingTestSequence
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/remote_casting/audio/
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=remote_casting_joplin
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+# Never bactrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/assistant_remote_casting/remote_casting_config.json b/lab_system/master/config/tests/assistant_remote_casting/remote_casting_config.json
new file mode 100644
index 0000000..6cb2d56
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_remote_casting/remote_casting_config.json
@@ -0,0 +1,13 @@
+{
+  "remote_audio": {
+    "devices": {
+      "source": {
+        "ip": "100.107.118.94"
+      },
+      "target": {
+        "friendly_name": "kitchen",
+        "ip": "100.107.118.100"
+      }
+    }
+  }
+}
diff --git a/lab_system/master/config/tests/assistant_remote_casting/test_defs.json b/lab_system/master/config/tests/assistant_remote_casting/test_defs.json
new file mode 100644
index 0000000..cf50c75
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_remote_casting/test_defs.json
@@ -0,0 +1,25 @@
+ [
+  {
+    "group_name": "assistant_playback",
+    "slave_name": "assistant_playback",
+    "test_name": "assistant_remote_casting_latency",
+    "identifier": "assistant_remote_casting_latency",
+    "device_type": "joplin",
+    "device_group": "assistant_remote_casting_joplin",
+    "build_flavor": "user",
+    "branch": ["next_release", "release"],
+    "num_devices": "2",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "StrictDeviceAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/tests/assistant_stop_command_test/stop_command_test.cfg b/lab_system/master/config/tests/assistant_stop_command_test/stop_command_test.cfg
new file mode 100644
index 0000000..7abbe5a
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_stop_command_test/stop_command_test.cfg
@@ -0,0 +1,41 @@
+# Config file for assistant latency tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_stop_command_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantStopCommandTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.query_driven_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=QueryDrivenTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/actions_accepting_stop_command
+
+# This identifier will appear as a prefix on the name of test result.
+# Valid only for tests that uses QueryDrivenTestSequence, otherwise ignored.
+TEST_RESULT_PREFIX=stop_command_tests
+
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+BUILD_SELECTION_POLICY=backtrack_then_latest
+
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
+
diff --git a/lab_system/master/config/tests/assistant_timer_test/assistant_timer_dialogue_test.cfg b/lab_system/master/config/tests/assistant_timer_test/assistant_timer_dialogue_test.cfg
new file mode 100644
index 0000000..7edf50b
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_timer_test/assistant_timer_dialogue_test.cfg
@@ -0,0 +1,37 @@
+# Config file for follow-up query tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_timer_dialogue_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantTimerDialogueTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/dialogue/timer
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+BUILD_SELECTION_POLICY=backtrack_then_latest
+
+# Maximum number of builds to be used in build selection process.
+MAX_BUILD_SELECTION_RANGE=3
+
+# Use Tunnel where applicable
+
diff --git a/lab_system/master/config/tests/assistant_wer_test/assistant_art_wer_originals_test.cfg b/lab_system/master/config/tests/assistant_wer_test/assistant_art_wer_originals_test.cfg
new file mode 100644
index 0000000..9983d05
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_wer_test/assistant_art_wer_originals_test.cfg
@@ -0,0 +1,28 @@
+# Config file for Word Error Rate (WER) tests.
+#
+# Do not modify property names
+# PROPERTIES
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/wer_samples_art_normalized/
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_wer_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantWerTestCase
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantWerTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_wer_test_sequence
+
+
diff --git a/lab_system/master/config/tests/assistant_wer_test/assistant_art_wer_originals_test_fuchsia.cfg b/lab_system/master/config/tests/assistant_wer_test/assistant_art_wer_originals_test_fuchsia.cfg
new file mode 100644
index 0000000..0590910
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_wer_test/assistant_art_wer_originals_test_fuchsia.cfg
@@ -0,0 +1,28 @@
+# Config file for Word Error Rate (WER) tests.
+#
+# Do not modify property names
+# PROPERTIES
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/wer_samples_art_normalized/
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_wer_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantWerTestCase
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaAssistantWerTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.fuchsia_assistant_wer_test_sequence
+
+
diff --git a/lab_system/master/config/tests/assistant_wer_test/assistant_wer_test.cfg b/lab_system/master/config/tests/assistant_wer_test/assistant_wer_test.cfg
new file mode 100644
index 0000000..85019b7
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_wer_test/assistant_wer_test.cfg
@@ -0,0 +1,28 @@
+# Config file for Word Error Rate (WER) tests.
+#
+# Do not modify property names
+# PROPERTIES
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/wer_samples
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_wer_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantWerTestCase
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AssistantWerTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_wer_test_sequence
+
+
diff --git a/lab_system/master/config/tests/assistant_wer_test/assistant_wer_test_fuchsia.cfg b/lab_system/master/config/tests/assistant_wer_test/assistant_wer_test_fuchsia.cfg
new file mode 100644
index 0000000..eab5ccd
--- /dev/null
+++ b/lab_system/master/config/tests/assistant_wer_test/assistant_wer_test_fuchsia.cfg
@@ -0,0 +1,28 @@
+# Config file for Word Error Rate (WER) tests.
+#
+# Do not modify property names
+# PROPERTIES
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/wer_samples
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.assistant_wer_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AssistantWerTestCase
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaAssistantWerTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.fuchsia_assistant_wer_test_sequence
+
+
diff --git a/lab_system/master/config/tests/atv_unit_test/test_defs.json b/lab_system/master/config/tests/atv_unit_test/test_defs.json
new file mode 100644
index 0000000..1334dc5
--- /dev/null
+++ b/lab_system/master/config/tests/atv_unit_test/test_defs.json
@@ -0,0 +1,30 @@
+[
+    {
+        "branch": ["master", "release", "next_release"],
+        "build_flavor": "Release",
+	"device_group": "unit_test",
+        "device_type": "fugu",
+        "group_name": "fugu_unit_test",
+        "identifier": "fugu",
+        "scheduler": "*/10 * * * *",
+        "slave_name": "fugu",
+        "slave_number": [
+            1
+        ],
+        "step_factories_recipe": "step_factories_recipes/atv_unit_test_recipe.json"
+    },
+    {
+        "branch": ["master", "release", "next_release"],
+	"build_flavor": "Release",
+        "device_group": "unit_test",
+        "device_type": "darcy",
+        "group_name": "fugu_unit_test",
+        "identifier": "shield_tv",
+        "scheduler": "*/10 * * * *",
+        "slave_name": "shield_tv",
+        "slave_number": [
+            1
+        ],
+        "step_factories_recipe": "step_factories_recipes/atv_unit_test_recipe.json"
+    }
+]
diff --git a/lab_system/master/config/tests/audio_formats/audio_formats.cfg b/lab_system/master/config/tests/audio_formats/audio_formats.cfg
new file mode 100644
index 0000000..503d7e2
--- /dev/null
+++ b/lab_system/master/config/tests/audio_formats/audio_formats.cfg
@@ -0,0 +1,15 @@
+# Config file for JavaScript audio format tests.
+#
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.audio_formats.audio_formats_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SupportedAudioFormatsTestCase,SupportedAudioFormatsHiResTestCase,AudioBitRatesTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+
diff --git a/lab_system/master/config/tests/audio_formats/audio_formats_mclean.cfg b/lab_system/master/config/tests/audio_formats/audio_formats_mclean.cfg
new file mode 100644
index 0000000..763d4a5
--- /dev/null
+++ b/lab_system/master/config/tests/audio_formats/audio_formats_mclean.cfg
@@ -0,0 +1,15 @@
+# Config file for JavaScript audio format tests. Different group.
+#
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.audio_formats.audio_formats_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SupportedAudioFormatsTestCase,SupportedAudioFormatsHiResTestCase,AudioBitRatesTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+
diff --git a/lab_system/master/config/tests/audio_passthrough/audio_passthrough.cfg b/lab_system/master/config/tests/audio_passthrough/audio_passthrough.cfg
new file mode 100644
index 0000000..a610265
--- /dev/null
+++ b/lab_system/master/config/tests/audio_passthrough/audio_passthrough.cfg
@@ -0,0 +1,15 @@
+# Config file for JavaScript audio passthrough tests.
+#
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.audio_passthrough.audio_passthrough_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AudioPassthroughFormatsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+
diff --git a/lab_system/master/config/tests/audiodefault_unit_test/test_defs.json b/lab_system/master/config/tests/audiodefault_unit_test/test_defs.json
new file mode 100644
index 0000000..f163f53
--- /dev/null
+++ b/lab_system/master/config/tests/audiodefault_unit_test/test_defs.json
@@ -0,0 +1,42 @@
+[
+    {
+        "branch": ["cast_audio_reference_master", "cast_audio_reference_release"],
+        "build_flavor": "eng",
+        "device_group": "unit_test",
+        "device_type": "audiodefault",
+        "identifier": "audiodefault_eng",
+        "scheduler": "*/10 * * * *",
+        "slave_name": "audiodefault_eng",
+        "slave_number": [
+            1
+        ],
+        "step_factories_recipe": "step_factories_recipes/audiodefault_unit_test_recipe.json"
+    },
+    {
+        "branch": ["cast_assistant_reference_master", "cast_assistant_reference_release"],
+        "build_flavor": "eng",
+        "device_group": "unit_test",
+        "device_type": "assistantdefault",
+        "hidden": "true",
+        "identifier": "assistant_reference_eng",
+        "scheduler": "*/10 * * * *",
+        "slave_name": "assistant_reference_eng",
+        "slave_number": [
+            1
+        ],
+        "step_factories_recipe": "step_factories_recipes/audiodefault_unit_test_recipe.json"
+    },
+    {
+        "branch": ["cast_audio_mediatek_master", "cast_audio_mediatek_release", "cast_audio_mediatek_next_release"],
+        "build_flavor": "eng",
+        "device_group": "unit_test",
+        "device_type": "mozart",
+        "identifier": "mozart_eng",
+        "scheduler": "*/10 * * * *",
+        "slave_name": "mozart_eng",
+        "slave_number": [
+            1
+        ],
+        "step_factories_recipe": "step_factories_recipes/claim_hint_unit_test_recipe.json"
+    }
+]
diff --git a/lab_system/master/config/tests/av_sync/atv_av_sync_720p_30fps.cfg b/lab_system/master/config/tests/av_sync/atv_av_sync_720p_30fps.cfg
new file mode 100644
index 0000000..85eef7c
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/atv_av_sync_720p_30fps.cfg
@@ -0,0 +1,35 @@
+# Config file for presence test using conducted audio on android devices
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.av_sync_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AVSyncTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidAvQualityTestSequence
+
+# Specify the av channel based on video and testbed
+AUDIO_CHANNEL=0
+VIDEO_CHANNEL=7
+
+# Test parameters depending on the video provided above
+NOISE_MEASUREMENT_DURATION_SEC=30
+PLAY_DURATION_NO_ACTION_SEC=300
+SEEK_FORWARD_SEC=10
+SYNC_MEASUREMENT_AFTER_ACTION_SEC=32
+ITERATION_PAUSE=10
+ITERATION_SEEK=10
+DURATION_BEFORE_RESUME_SEC=32
+
+TEST_CASE_PREFIX=AVSync
+VIDEO_SPECIFIERS=720P30_H264_2M
diff --git a/lab_system/master/config/tests/av_sync/atv_av_sync_720p_60fps.cfg b/lab_system/master/config/tests/av_sync/atv_av_sync_720p_60fps.cfg
new file mode 100644
index 0000000..7db2d7e
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/atv_av_sync_720p_60fps.cfg
@@ -0,0 +1,35 @@
+# Config file for presence test using conducted audio on android devices
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.av_sync_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AVSyncTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidAvQualityTestSequence
+
+# Specify the av channel based on video and testbed
+AUDIO_CHANNEL=0
+VIDEO_CHANNEL=7
+
+# Test parameters depending on the video provided above
+NOISE_MEASUREMENT_DURATION_SEC=30
+PLAY_DURATION_NO_ACTION_SEC=300
+SEEK_FORWARD_SEC=10
+SYNC_MEASUREMENT_AFTER_ACTION_SEC=32
+ITERATION_PAUSE=10
+ITERATION_SEEK=10
+DURATION_BEFORE_RESUME_SEC=32
+
+TEST_CASE_PREFIX=AVSync
+VIDEO_SPECIFIERS=720P60_H264_2M,720P60_VP9_2M,720P60_VP8_2M,720P60_H265_2M,720P60_H265_2M_HDR10
diff --git a/lab_system/master/config/tests/av_sync/atv_freezing_720p_60fps.cfg b/lab_system/master/config/tests/av_sync/atv_freezing_720p_60fps.cfg
new file mode 100644
index 0000000..5a0105e
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/atv_freezing_720p_60fps.cfg
@@ -0,0 +1,25 @@
+# Config file for video freezing test on Android TV devices
+# The test evaluate frame drop and duplication with different video codec,
+# resolutions and bit rates.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.video_freezing_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=VideoFreezingTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidAvQualityTestSequence
+
+# Test parameters
+PLAYBACK_DURATION_SEC=900
+RECORD_DELAY_SEC=20
+WAIT_FOR_BUFFER_SEC=30
+
+TEST_CASE_PREFIX=Freezing
+VIDEO_SPECIFIERS=720P60_H264_2M,720P60_VP9_2M,720P60_VP8_2M,720P60_H265_2M,720P60_H265_2M_HDR10
diff --git a/lab_system/master/config/tests/av_sync/atv_mz_av_sync_720p_60fps.cfg b/lab_system/master/config/tests/av_sync/atv_mz_av_sync_720p_60fps.cfg
new file mode 100644
index 0000000..e6b04d9
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/atv_mz_av_sync_720p_60fps.cfg
@@ -0,0 +1,35 @@
+# Config file for presence test using conducted audio on android devices
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.av_sync_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MultizoneAVSyncTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidAvQualityTestSequence
+
+# Specify the av channel based on video and testbed
+AUDIO_CHANNEL=0
+VIDEO_CHANNEL=7
+
+# Test parameters depending on the video provided above
+NOISE_MEASUREMENT_DURATION_SEC=30
+PLAY_DURATION_NO_ACTION_SEC=300
+SEEK_FORWARD_SEC=10
+SYNC_MEASUREMENT_AFTER_ACTION_SEC=32
+ITERATION_PAUSE=10
+ITERATION_SEEK=10
+DURATION_BEFORE_RESUME_SEC=32
+
+TEST_CASE_PREFIX=AVSync
+VIDEO_SPECIFIERS=720P60_H264_2M,720P60_VP9_2M,720P60_VP8_2M,720P60_H265_2M,720P60_H265_2M_HDR10
diff --git a/lab_system/master/config/tests/av_sync/atv_smoothness_720p_60fps.cfg b/lab_system/master/config/tests/av_sync/atv_smoothness_720p_60fps.cfg
new file mode 100644
index 0000000..924881f
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/atv_smoothness_720p_60fps.cfg
@@ -0,0 +1,26 @@
+# Config file for presence test using conducted audio on android devices
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.video_smoothness_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=VideoSmoothnessTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidAvQualityTestSequence
+
+# Test parameters depending on the video provided above
+NOISE_MEASUREMENT_DURATION_SEC=10
+PLAY_DURATION_SEC=300
+
+TEST_CASE_PREFIX=Smoothness
+VIDEO_SPECIFIERS=720P60_H264_2M,720P60_VP9_2M,720P60_H265_2M,720P60_H265_2M_HDR10
diff --git a/lab_system/master/config/tests/av_sync/cast_av_sync_720p_30fps.cfg b/lab_system/master/config/tests/av_sync/cast_av_sync_720p_30fps.cfg
new file mode 100644
index 0000000..e136333
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/cast_av_sync_720p_30fps.cfg
@@ -0,0 +1,33 @@
+# Config file for avsync test on cast devices setup with barcode testbed
+# Vertical barcode video is used correspond to the refresh orientation
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class
+TEST_CASE_MODULE=eurtest.tests.avsync.av_sync_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AVSyncTestCase
+
+# Dot-notation path to the module containing the TestSequence class
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class
+TEST_SEQUENCE_CLASS=AvQualityTestSequence
+
+# Specify the av channel based on video and testbed
+AUDIO_CHANNEL=0
+VIDEO_CHANNEL=7
+
+# Test parameters depending on the video provided above
+NOISE_MEASUREMENT_DURATION_SEC=10
+PLAY_DURATION_NO_ACTION_SEC=300
+SEEK_FORWARD_SEC=10
+SYNC_MEASUREMENT_AFTER_ACTION_SEC=32
+ITERATION_PAUSE=10
+ITERATION_SEEK=10
+DURATION_BEFORE_RESUME_SEC=32
+
+SIGNED_IN=True
+TEST_CASE_PREFIX=AVSync
+VIDEO_SPECIFIERS=720P30_H264_2M_VERTICAL
diff --git a/lab_system/master/config/tests/av_sync/cast_av_sync_720p_60fps.cfg b/lab_system/master/config/tests/av_sync/cast_av_sync_720p_60fps.cfg
new file mode 100644
index 0000000..1fb07c1
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/cast_av_sync_720p_60fps.cfg
@@ -0,0 +1,33 @@
+# Config file for avsync test on cast devices setup with barcode testbed
+# Vertical barcode video is used correspond to the refresh orientation
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class
+TEST_CASE_MODULE=eurtest.tests.avsync.av_sync_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AVSyncTestCase
+
+# Dot-notation path to the module containing the TestSequence class
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class
+TEST_SEQUENCE_CLASS=AvQualityTestSequence
+
+# Specify the av channel based on video and testbed
+AUDIO_CHANNEL=0
+VIDEO_CHANNEL=7
+
+# Test parameters depending on the video provided above
+NOISE_MEASUREMENT_DURATION_SEC=10
+PLAY_DURATION_NO_ACTION_SEC=300
+SEEK_FORWARD_SEC=10
+SYNC_MEASUREMENT_AFTER_ACTION_SEC=32
+ITERATION_PAUSE=10
+ITERATION_SEEK=10
+DURATION_BEFORE_RESUME_SEC=32
+
+SIGNED_IN=True
+TEST_CASE_PREFIX=AVSync
+VIDEO_SPECIFIERS=720P60_H264_2M_VERTICAL
diff --git a/lab_system/master/config/tests/av_sync/cast_freezing_720p_60fps.cfg b/lab_system/master/config/tests/av_sync/cast_freezing_720p_60fps.cfg
new file mode 100644
index 0000000..26c5394
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/cast_freezing_720p_60fps.cfg
@@ -0,0 +1,25 @@
+# Config file for avsync test on cast devices setup with barcode testbed
+# Vertical barcode video is used correspond to the refresh orientation
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.video_freezing_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=VideoFreezingTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AvQualityTestSequence
+
+# Test parameters
+PLAYBACK_DURATION_SEC=900
+RECORD_DELAY_SEC=20
+WAIT_FOR_BUFFER_SEC=30
+
+SIGNED_IN=True
+TEST_CASE_PREFIX=Freezing
+VIDEO_SPECIFIERS=720P60_H264_2M_VERTICAL
diff --git a/lab_system/master/config/tests/av_sync/exoplayer_av_sync_720p_60fps.cfg b/lab_system/master/config/tests/av_sync/exoplayer_av_sync_720p_60fps.cfg
new file mode 100644
index 0000000..29cba3d
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/exoplayer_av_sync_720p_60fps.cfg
@@ -0,0 +1,36 @@
+# Config file for presence test using conducted audio on android devices
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.exoplayer_av_sync_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ExoplayerAVSyncTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidAvQualityTestSequence
+
+# Specify the av channel based on video and testbed
+AUDIO_CHANNEL=0
+VIDEO_CHANNEL=7
+
+# Test parameters depending on the video provided above
+NOISE_MEASUREMENT_DURATION_SEC=30
+PLAY_DURATION_NO_ACTION_SEC=300
+SEEK_FORWARD_SEC=10
+SYNC_MEASUREMENT_AFTER_ACTION_SEC=32
+ITERATION_PAUSE=10
+ITERATION_SEEK=10
+DURATION_BEFORE_RESUME_SEC=32
+
+TEST_CASE_PREFIX=AVSync
+VIDEO_SPECIFIERS=720P60_H264_2M,720P60_VP9_2M,720P60_VP8_2M,720P60_H265_2M,720P60_H265_2M_HDR10
+VIDEO_SPECIFIERS_BOREAL=720P60_AV1_2M
diff --git a/lab_system/master/config/tests/av_sync/exoplayer_freezing_720p_60fps.cfg b/lab_system/master/config/tests/av_sync/exoplayer_freezing_720p_60fps.cfg
new file mode 100644
index 0000000..989aa91
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/exoplayer_freezing_720p_60fps.cfg
@@ -0,0 +1,32 @@
+# Config file for presence test using conducted audio on android devices
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.exoplayer_video_freezing_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ExoplayerVideoFreezingTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidAvQualityTestSequence
+
+# Specify the av channel based on video and testbed
+AUDIO_CHANNEL=0
+VIDEO_CHANNEL=7
+
+# Test parameters
+PLAYBACK_DURATION_SEC=900
+RECORD_DELAY_SEC=20
+WAIT_FOR_BUFFER_SEC=0
+
+TEST_CASE_PREFIX=Freezing
+VIDEO_SPECIFIERS=720P60_H264_2M,720P60_VP9_2M,720P60_VP8_2M,720P60_H265_2M,720P60_H265_2M_HDR10
+VIDEO_SPECIFIERS_BOREAL=720P60_AV1_2M
diff --git a/lab_system/master/config/tests/av_sync/gq_live_view_quality.cfg b/lab_system/master/config/tests/av_sync/gq_live_view_quality.cfg
new file mode 100644
index 0000000..45f0953
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/gq_live_view_quality.cfg
@@ -0,0 +1,37 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.av_live_view_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AVLiveViewTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_live_view_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AVLiveViewTestSequence
+
+# The build that needs to be installed on each display type
+# The master builds are for local streaming tests
+ELAINE_1_56=gs://gtv-eureka/internal/1.56/elaine-user/265669
+NEWMAN_1_56=gs://gtv-eureka/internal/1.56/newman-user/265669
+ELAINE_MASTER=gs://gtv-eureka/internal/master/elaine-user/271523
+NEWMAN_MASTER=gs://gtv-eureka/internal/master/newman-user/271523
+
+# Test identifiers for all scenarios
+# Each Key should be usnique and starts with 'TEST_SCENARIO_' prefix
+# Format:<battery/line>-power_<high/low>-lux_<30/15>-fps_<premium/balanced>-quality_nv-<on/off>_talkback-<on/off>_<local/remote>-streaming
+# Example: line-power_high-lux_30-fps_premium-quality_nv-off_talkback-off_remote-streaming
+TEST_SCENARIO_1=battery-power_high-lux_30-fps_premium-quality_nv-off_talkback-on_remote-streaming
+TEST_SCENARIO_2=battery-power_low-lux_15-fps_premium-quality_nv-off_talkback-on_remote-streaming
+TEST_SCENARIO_3=battery-power_high-lux_30-fps_premium-quality_nv-on_talkback-on_remote-streaming
+TEST_SCENARIO_4=line-power_high-lux_30-fps_premium-quality_nv-off_talkback-on_remote-streaming
+TEST_SCENARIO_5=line-power_low-lux_15-fps_premium-quality_nv-off_talkback-on_remote-streaming
+TEST_SCENARIO_6=line-power_high-lux_30-fps_premium-quality_nv-on_talkback-on_remote-streaming
diff --git a/lab_system/master/config/tests/av_sync/legacy_av_sync_1080p_60fps.cfg b/lab_system/master/config/tests/av_sync/legacy_av_sync_1080p_60fps.cfg
new file mode 100644
index 0000000..c7dbe00
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/legacy_av_sync_1080p_60fps.cfg
@@ -0,0 +1,36 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.av_sync_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AVSyncTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AvQualityTestSequence
+
+# Specify the av channel based on video and testbed
+AUDIO_CHANNEL=0
+VIDEO_CHANNEL=1
+
+# Test parameters depending on the video provided above
+NOISE_MEASUREMENT_DURATION_SEC=10
+PLAY_DURATION_NO_ACTION_SEC=300
+SEEK_FORWARD_SEC=14
+SYNC_MEASUREMENT_AFTER_ACTION_SEC=30
+ITERATION_PAUSE=15
+ITERATION_SEEK=10
+DURATION_BEFORE_RESUME_SEC=30
+
+SIGNED_IN=True
+TEST_CASE_PREFIX=AVSync
+VIDEO_SPECIFIERS=LEGACY_1080P60_AVSYNC
diff --git a/lab_system/master/config/tests/av_sync/legacy_av_sync_1080p_60fps_180phase.cfg b/lab_system/master/config/tests/av_sync/legacy_av_sync_1080p_60fps_180phase.cfg
new file mode 100644
index 0000000..c44c6f7
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/legacy_av_sync_1080p_60fps_180phase.cfg
@@ -0,0 +1,36 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.av_sync_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AVSyncTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AvQualityTestSequence
+
+# Specify the av channel based on video and testbed
+AUDIO_CHANNEL=0
+VIDEO_CHANNEL=1
+
+# Test parameters depending on the video provided above
+NOISE_MEASUREMENT_DURATION_SEC=10
+PLAY_DURATION_NO_ACTION_SEC=300
+SEEK_FORWARD_SEC=14
+SYNC_MEASUREMENT_AFTER_ACTION_SEC=30
+ITERATION_PAUSE=15
+ITERATION_SEEK=10
+DURATION_BEFORE_RESUME_SEC=30
+
+SIGNED_IN=True
+TEST_CASE_PREFIX=AVSync
+VIDEO_SPECIFIERS=LEGACY_1080P60_AVSYNC_180PHASE
diff --git a/lab_system/master/config/tests/av_sync/legacy_av_sync_720p_30fps.cfg b/lab_system/master/config/tests/av_sync/legacy_av_sync_720p_30fps.cfg
new file mode 100644
index 0000000..0d978e1
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/legacy_av_sync_720p_30fps.cfg
@@ -0,0 +1,36 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.av_sync_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AVSyncTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AvQualityTestSequence
+
+# Specify the av channel based on video and testbed
+AUDIO_CHANNEL=0
+VIDEO_CHANNEL=1
+
+# Test parameters depending on the video provided above
+NOISE_MEASUREMENT_DURATION_SEC=10
+PLAY_DURATION_NO_ACTION_SEC=300
+SEEK_FORWARD_SEC=14
+SYNC_MEASUREMENT_AFTER_ACTION_SEC=30
+ITERATION_PAUSE=15
+ITERATION_SEEK=10
+DURATION_BEFORE_RESUME_SEC=30
+
+SIGNED_IN=True
+TEST_CASE_PREFIX=AVSync
+VIDEO_SPECIFIERS=LEGACY_720P30_AVSYNC
diff --git a/lab_system/master/config/tests/av_sync/legacy_av_sync_720p_60fps.cfg b/lab_system/master/config/tests/av_sync/legacy_av_sync_720p_60fps.cfg
new file mode 100644
index 0000000..b999697
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/legacy_av_sync_720p_60fps.cfg
@@ -0,0 +1,36 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.av_sync_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AVSyncTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AvQualityTestSequence
+
+# Specify the av channel based on video and testbed
+AUDIO_CHANNEL=0
+VIDEO_CHANNEL=1
+
+# Test parameters depending on the video provided above
+NOISE_MEASUREMENT_DURATION_SEC=10
+PLAY_DURATION_NO_ACTION_SEC=300
+SEEK_FORWARD_SEC=14
+SYNC_MEASUREMENT_AFTER_ACTION_SEC=30
+ITERATION_PAUSE=15
+ITERATION_SEEK=10
+DURATION_BEFORE_RESUME_SEC=30
+
+SIGNED_IN=True
+TEST_CASE_PREFIX=AVSync
+VIDEO_SPECIFIERS=LEGACY_720P60_AVSYNC
diff --git a/lab_system/master/config/tests/av_sync/legacy_smoothness_720p_60fps.cfg b/lab_system/master/config/tests/av_sync/legacy_smoothness_720p_60fps.cfg
new file mode 100644
index 0000000..73e8406
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/legacy_smoothness_720p_60fps.cfg
@@ -0,0 +1,27 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.video_smoothness_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=VideoSmoothnessTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AvQualityTestSequence
+
+# Test parameters depending on the video provided above
+NOISE_MEASUREMENT_DURATION_SEC=0
+PLAY_DURATION_SEC=300
+
+SIGNED_IN=True
+TEST_CASE_PREFIX=Smoothness
+VIDEO_SPECIFIERS=LEGACY_720P60_SMOOTHNESS
diff --git a/lab_system/master/config/tests/av_sync/nq_live_view_quality.cfg b/lab_system/master/config/tests/av_sync/nq_live_view_quality.cfg
new file mode 100644
index 0000000..cc1d7d5
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/nq_live_view_quality.cfg
@@ -0,0 +1,37 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.av_live_view_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AVLiveViewTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_live_view_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AVLiveViewTestSequence
+
+# The build that needs to be installed on each display type
+# The master builds are for local streaming tests
+ELAINE_1_56=gs://gtv-eureka/internal/1.56/elaine-user/265669
+NEWMAN_1_56=gs://gtv-eureka/internal/1.56/newman-user/265669
+ELAINE_MASTER=gs://gtv-eureka/internal/master/elaine-user/271523
+NEWMAN_MASTER=gs://gtv-eureka/internal/master/newman-user/271523
+
+# Test identifiers for all scenarios
+# Each Key should be usnique and starts with 'TEST_SCENARIO_' prefix
+# Format:<battery/line>-power_<high/low>-lux_<30/15>-fps_<premium/balanced>-quality_nv-<on/off>_talkback-<on/off>_<local/remote>-streaming
+# Example: line-power_high-lux_30-fps_premium-quality_nv-off_talkback-off_remote-streaming
+TEST_SCENARIO_1=battery-power_high-lux_30-fps_premium-quality_nv-off_talkback-off_remote-streaming
+TEST_SCENARIO_2=battery-power_low-lux_15-fps_premium-quality_nv-off_talkback-off_remote-streaming
+TEST_SCENARIO_3=battery-power_high-lux_30-fps_premium-quality_nv-on_talkback-off_remote-streaming
+TEST_SCENARIO_4=line-power_high-lux_30-fps_premium-quality_nv-off_talkback-off_remote-streaming
+TEST_SCENARIO_5=line-power_low-lux_15-fps_premium-quality_nv-off_talkback-off_remote-streaming
+TEST_SCENARIO_6=line-power_high-lux_30-fps_premium-quality_nv-on_talkback-off_remote-streaming
diff --git a/lab_system/master/config/tests/av_sync/spencer_live_view_quality.cfg b/lab_system/master/config/tests/av_sync/spencer_live_view_quality.cfg
new file mode 100644
index 0000000..b74129d
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/spencer_live_view_quality.cfg
@@ -0,0 +1,34 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.av_live_view_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AVLiveViewTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_live_view_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AVLiveViewTestSequence
+
+# The build that needs to be installed on each display type
+# The master builds are for local streaming tests
+ELAINE_1_56=gs://gtv-eureka/internal/1.56/elaine-user/265669
+NEWMAN_1_56=gs://gtv-eureka/internal/1.56/newman-user/265669
+ELAINE_MASTER=gs://gtv-eureka/internal/master/elaine-user/271523
+NEWMAN_MASTER=gs://gtv-eureka/internal/master/newman-user/271523
+
+# Test identifiers for all scenarios
+# Each Key should be usnique and starts with 'TEST_SCENARIO_' prefix
+# Format:<battery/line>-power_<high/low>-lux_<30/15>-fps_<premium/balanced>-quality_nv-<on/off>_talkback-<on/off>_<local/remote>-streaming
+# Example: line-power_high-lux_30-fps_premium-quality_nv-off_talkback-off_remote-streaming
+TEST_SCENARIO_1=line-power_high-lux_30-fps_premium-quality_nv-off_talkback-off_remote-streaming
+TEST_SCENARIO_2=line-power_low-lux_15-fps_premium-quality_nv-off_talkback-off_remote-streaming
+TEST_SCENARIO_3=line-power_high-lux_30-fps_premium-quality_nv-on_talkback-off_remote-streaming
diff --git a/lab_system/master/config/tests/av_sync/venus_live_view_quality.cfg b/lab_system/master/config/tests/av_sync/venus_live_view_quality.cfg
new file mode 100644
index 0000000..214219d
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/venus_live_view_quality.cfg
@@ -0,0 +1,26 @@
+# Config file for live view quality test on Venus.
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.av_live_view_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AVLiveViewTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_live_view_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AVLiveViewTestSequence
+
+# The build that needs to be installed on each display type
+# The master builds are for local streaming tests
+ELAINE_1_56=gs://gtv-eureka/internal/1.56.3/elaine-user/308003
+ELAINE_MASTER=gs://gtv-eureka/internal/master/elaine-user/271523
+
+# Test identifiers for all scenarios
+# Each Key should be usnique and starts with 'TEST_SCENARIO_' prefix
+# Format:<battery/line>-power_<high/low>-lux_<30/15>-fps_<premium/balanced>-quality_nv-<on/off>_talkback-<on/off>_<local/remote>-streaming
+# Example: line-power_high-lux_30-fps_premium-quality_nv-off_talkback-off_remote-streaming
+TEST_SCENARIO_1=line-power_high-lux_30-fps_premium-quality_nv-off_talkback-on_remote-streaming
+TEST_SCENARIO_2=line-power_low-lux_15-fps_premium-quality_nv-off_talkback-on_remote-streaming
+TEST_SCENARIO_3=line-power_high-lux_30-fps_premium-quality_nv-on_talkback-on_remote-streaming
diff --git a/lab_system/master/config/tests/av_sync/youtube_av_sync_720p_60fps.cfg b/lab_system/master/config/tests/av_sync/youtube_av_sync_720p_60fps.cfg
new file mode 100644
index 0000000..a1b9283
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/youtube_av_sync_720p_60fps.cfg
@@ -0,0 +1,35 @@
+# Config file for presence test using conducted audio on android devices
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.youtube_av_sync_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=YoutubeAVSyncTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidAvQualityTestSequence
+
+# Specify the av channel based on video and testbed
+AUDIO_CHANNEL=0
+VIDEO_CHANNEL=7
+
+# Test parameters depending on the video provided above
+NOISE_MEASUREMENT_DURATION_SEC=30
+PLAY_DURATION_NO_ACTION_SEC=300
+SEEK_FORWARD_SEC=10
+SYNC_MEASUREMENT_AFTER_ACTION_SEC=32
+ITERATION_PAUSE=10
+ITERATION_SEEK=10
+DURATION_BEFORE_RESUME_SEC=32
+
+TEST_CASE_PREFIX=AVSync
+VIDEO_SPECIFIERS=720P60_YOUTUBE_2M
diff --git a/lab_system/master/config/tests/av_sync/youtube_freezing_720p_60fps.cfg b/lab_system/master/config/tests/av_sync/youtube_freezing_720p_60fps.cfg
new file mode 100644
index 0000000..1e9e03a
--- /dev/null
+++ b/lab_system/master/config/tests/av_sync/youtube_freezing_720p_60fps.cfg
@@ -0,0 +1,31 @@
+# Config file for presence test using conducted audio on android devices
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.avsync.youtube_video_freezing_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=YoutubeVideoFreezingTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.avsync.av_quality_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidAvQualityTestSequence
+
+# Specify the av channel based on video and testbed
+AUDIO_CHANNEL=0
+VIDEO_CHANNEL=7
+
+# Test parameters
+PLAYBACK_DURATION_SEC=900
+RECORD_DELAY_SEC=20
+WAIT_FOR_BUFFER_SEC=0
+
+TEST_CASE_PREFIX=Freezing
+VIDEO_SPECIFIERS=720P60_YOUTUBE_2M
diff --git a/lab_system/master/config/tests/bluetooth_a2dp/bluetooth_a2dp.cfg b/lab_system/master/config/tests/bluetooth_a2dp/bluetooth_a2dp.cfg
new file mode 100644
index 0000000..ec6478a
--- /dev/null
+++ b/lab_system/master/config/tests/bluetooth_a2dp/bluetooth_a2dp.cfg
@@ -0,0 +1,19 @@
+# Config file for Bluetooth A2DP test suite.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.bluetooth_a2dp.bluetooth_a2dp_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=BluetoothDiscoveryTestCases,BluetoothApiTestCases,BluetoothStreamingTestCases
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
diff --git a/lab_system/master/config/tests/bluetooth_a2dp/test_defs.json b/lab_system/master/config/tests/bluetooth_a2dp/test_defs.json
new file mode 100644
index 0000000..8b71a2f
--- /dev/null
+++ b/lab_system/master/config/tests/bluetooth_a2dp/test_defs.json
@@ -0,0 +1,74 @@
+[
+  {
+    "group_name": "bluetooth_a2dp",
+    "slave_name": "bluetooth_a2dp_enclosure",
+    "test_name": "bluetooth_a2dp",
+    "identifier": "bluetooth_a2dp_chirp",
+    "device_type": "chirp_b3",
+    "device_group": "setup_enclosure_chirp",
+    "branch": ["master"],
+    "build_flavor": "user",
+    "scheduler": "0 6,15,8 * * *",
+    "step_factories_recipe": "step_factories_recipes/bringup_ethernet_port.json"
+  },
+  {
+    "group_name": "bluetooth_a2dp",
+    "slave_name": "bluetooth_a2dp_enclosure",
+    "test_name": "bluetooth_a2dp",
+    "identifier": "bluetooth_a2dp_chirp",
+    "device_type": "chirp_b3",
+    "device_group": "setup_enclosure_chirp",
+    "branch": ["next_release"],
+    "build_flavor": "user",
+    "scheduler": "0 7,16,17 * * *",
+    "step_factories_recipe": "step_factories_recipes/bringup_ethernet_port.json"
+  },
+  {
+    "group_name": "bluetooth_a2dp",
+    "slave_name": "bluetooth_a2dp_enclosure",
+    "test_name": "bluetooth_a2dp",
+    "identifier": "bluetooth_a2dp_joplin",
+    "device_type": "joplin",
+    "device_group": "setup_enclosure_joplin",
+    "branch": ["master"],
+    "build_flavor": "user",
+    "scheduler": "0 6,15,8 * * *",
+    "step_factories_recipe": "step_factories_recipes/bringup_ethernet_port.json"
+  },
+  {
+    "group_name": "bluetooth_a2dp",
+    "slave_name": "bluetooth_a2dp_enclosure",
+    "test_name": "bluetooth_a2dp",
+    "identifier": "bluetooth_a2dp_joplin",
+    "device_type": "joplin",
+    "device_group": "setup_enclosure_joplin",
+    "branch": ["next_release"],
+    "build_flavor": "user",
+    "scheduler": "0 7,16,17 * * *",
+    "step_factories_recipe": "step_factories_recipes/bringup_ethernet_port.json"
+  },
+  {
+    "group_name": "bluetooth_a2dp",
+    "slave_name": "bluetooth_a2dp_enclosure",
+    "test_name": "bluetooth_a2dp",
+    "identifier": "bluetooth_a2dp_biggie",
+    "device_type": "biggie",
+    "device_group": "setup_enclosure_biggie",
+    "branch": ["master"],
+    "build_flavor": "user",
+    "scheduler": "0 6,15,8 * * *",
+    "step_factories_recipe": "step_factories_recipes/bringup_ethernet_port.json"
+  },
+  {
+    "group_name": "bluetooth_a2dp",
+    "slave_name": "bluetooth_a2dp_enclosure",
+    "test_name": "bluetooth_a2dp",
+    "identifier": "bluetooth_a2dp_biggie",
+    "device_type": "biggie",
+    "device_group": "setup_enclosure_biggie",
+    "branch": ["next_release"],
+    "build_flavor": "user",
+    "scheduler": "0 7,16,17 * * *",
+    "step_factories_recipe": "step_factories_recipes/bringup_ethernet_port.json"
+  }
+]
diff --git a/lab_system/master/config/tests/build_upgrade/build_upgrade.cfg b/lab_system/master/config/tests/build_upgrade/build_upgrade.cfg
new file mode 100644
index 0000000..743dbea
--- /dev/null
+++ b/lab_system/master/config/tests/build_upgrade/build_upgrade.cfg
@@ -0,0 +1,15 @@
+# Config file for BuildUpgrade tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.fdr.build_upgrade_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=BuildUpgradeTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.fdr.fdr_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FdrTestSequence
diff --git a/lab_system/master/config/tests/build_upgrade/test_defs.json b/lab_system/master/config/tests/build_upgrade/test_defs.json
new file mode 100644
index 0000000..816baaf
--- /dev/null
+++ b/lab_system/master/config/tests/build_upgrade/test_defs.json
@@ -0,0 +1,30 @@
+[
+  {
+    "group_name": "build_upgrade",
+    "slave_name": "build_upgrade",
+    "test_name": "build_upgrade",
+    "identifier": "build_upgrade_v3",
+    "device_type": "earth",
+    "device_group": "earth_fdr",
+    "branch": ["master", "release", "on_demand"]
+  },
+  {
+    "group_name": "build_upgrade",
+    "slave_name": "build_upgrade",
+    "test_name": "build_upgrade",
+    "identifier": "build_upgrade_chirp",
+    "device_type": "chirp",
+    "device_group": "chirp",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "build_upgrade",
+    "slave_name": "build_upgrade",
+    "test_name": "build_upgrade",
+    "identifier": "build_upgrade_v4",
+    "device_type": "luther_b2",
+    "device_group": "luther_eth",
+    "branch": ["master", "release"],
+    "hidden": "true"
+  }
+]
diff --git a/lab_system/master/config/tests/cache_test/cache_test.cfg b/lab_system/master/config/tests/cache_test/cache_test.cfg
new file mode 100644
index 0000000..3a6bb81
--- /dev/null
+++ b/lab_system/master/config/tests/cache_test/cache_test.cfg
@@ -0,0 +1,16 @@
+# Config file for javascript CacheTestCase Suite
+# It still requires --disable-web-security flag.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cache.cache_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CacheTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
diff --git a/lab_system/master/config/tests/caf/caf_staging.cfg b/lab_system/master/config/tests/caf/caf_staging.cfg
new file mode 100644
index 0000000..a821744
--- /dev/null
+++ b/lab_system/master/config/tests/caf/caf_staging.cfg
@@ -0,0 +1,17 @@
+# Config file cafe2e tests using staging version of caf.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.caf.caf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CafBasicStagingTestCase,CafEventsStagingTestCase,CafFastplayStagingTestCase,CafInterceptorsStagingTestCase,CafLoadStagingTestCase,CafLoadWithInitialTimeStagingTestCase,CafPlayToEndStagingTestCase,CafQueuingStagingTestCase,CafSeekStagingTestCase,CafStitchingPlayerStagingTestCase,CafTracksStagingTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+
diff --git a/lab_system/master/config/tests/canvas_benchmarks/canvas_benchmarks.cfg b/lab_system/master/config/tests/canvas_benchmarks/canvas_benchmarks.cfg
new file mode 100644
index 0000000..fd7e6de
--- /dev/null
+++ b/lab_system/master/config/tests/canvas_benchmarks/canvas_benchmarks.cfg
@@ -0,0 +1,17 @@
+# Config file for the canvas render performance tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.canvas_benchmarks.canvas_render_performance_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CanvasRenderPerformanceTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+
diff --git a/lab_system/master/config/tests/canvas_mark/canvas_mark.cfg b/lab_system/master/config/tests/canvas_mark/canvas_mark.cfg
new file mode 100644
index 0000000..37e5c2d
--- /dev/null
+++ b/lab_system/master/config/tests/canvas_mark/canvas_mark.cfg
@@ -0,0 +1,17 @@
+# Config file for Canvas Mark benchmark tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.canvas_mark.canvas_mark_benchmark_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CanvasMarkBenchmarkTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.canvas_mark.canvas_mark_benchmark_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CanvasMarkBenchmarkTestSequence
+
+
diff --git a/lab_system/master/config/tests/canvas_mark/test_defs.json b/lab_system/master/config/tests/canvas_mark/test_defs.json
new file mode 100644
index 0000000..8a27f88
--- /dev/null
+++ b/lab_system/master/config/tests/canvas_mark/test_defs.json
@@ -0,0 +1,105 @@
+[
+  {
+    "group_name": "canvas_mark",
+    "slave_name": "octane_canvas_mark",
+    "test_name": "canvas_mark",
+    "identifier": "canvas_mark",
+    "device_type": "eureka",
+    "device_group": "eureka_debug",
+    "branch": ["master", "release", "1_22"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "canvas_mark",
+    "slave_name": "octane_canvas_mark",
+    "test_name": "canvas_mark",
+    "identifier": "canvas_mark_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "branch": ["master", "release", "1_22"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "canvas_mark",
+    "slave_name": "octane_canvas_mark",
+    "test_name": "canvas_mark",
+    "identifier": "canvas_mark_v3",
+    "device_type": "earth",
+    "device_group": "earth_eth",
+    "branch": ["master", "release", "1_22"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "canvas_mark",
+    "slave_name": "octane_canvas_mark",
+    "test_name": "canvas_mark",
+    "identifier": "canvas_mark_v4",
+    "device_type": "luther_b2",
+    "device_group": "luther_eth",
+    "branch": ["master", "release", "1_22"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "canvas_mark",
+    "slave_name": "eurtest_canvas_mark",
+    "test_name": "canvas_mark",
+    "identifier": "canvas_mark_bubbletea",
+    "device_type": "bubbletea",
+    "device_group": "bubbletea",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  },
+  {
+    "group_name": "canvas_mark",
+    "slave_name": "eurtest_canvas_mark",
+    "test_name": "canvas_mark",
+    "identifier": "canvas_mark_entourage",
+    "device_type": "dragonfly",
+    "device_group": "entourage",
+    "branch": ["cast_tv_dragonfly_master", "cast_tv_dragonfly_release", "cast_tv_dragonfly_next_release"]
+  },
+  {
+    "group_name": "canvas_mark",
+    "slave_name": "eurtest_canvas_mark",
+    "test_name": "canvas_mark",
+    "identifier": "canvas_mark_guabao",
+    "device_type": "guabao",
+    "device_group": "entourage",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  },
+  {
+    "group_name": "canvas_mark",
+    "slave_name": "eurtest_canvas_mark",
+    "test_name": "canvas_mark",
+    "identifier": "canvas_mark_mars_us",
+    "device_type": "Mars_US",
+    "device_group": "entourage",
+    "branch": ["cast_tv_mediatek_master", "cast_tv_mediatek_release", "cast_tv_mediatek_next_release"]
+  },
+  {
+    "group_name": "canvas_mark",
+    "slave_name": "eurtest_canvas_mark",
+    "test_name": "canvas_mark",
+    "identifier": "canvas_mark_N00007",
+    "device_type": "N00007",
+    "device_group": "entourage",
+    "branch": ["cast_tv_novatek_master", "cast_tv_novatek_release", "cast_tv_novatek_next_release"]
+  },
+  {
+    "group_name": "canvas_mark",
+    "slave_name": "eurtest_canvas_mark",
+    "test_name": "canvas_mark",
+    "identifier": "canvas_mark_suncake",
+    "device_type": "suncake",
+    "device_group": "entourage",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  },
+  {
+    "group_name": "canvas_mark",
+    "slave_name": "eurtest_canvas_mark",
+    "test_name": "canvas_mark",
+    "identifier": "canvas_mark_taroball",
+    "device_type": "taroball",
+    "device_group": "entourage",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  }
+]
diff --git a/lab_system/master/config/tests/cast/atv_receiver.cfg b/lab_system/master/config/tests/cast/atv_receiver.cfg
new file mode 100644
index 0000000..93da18e
--- /dev/null
+++ b/lab_system/master/config/tests/cast/atv_receiver.cfg
@@ -0,0 +1,40 @@
+# Config file for the Cast receiver tests running on Android TV
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Remote Test Setup
+USER_NAME=gtvchrome
+REMOTE_TEST_ROOT=/home/gtvchrome/repositories/HEAD
+REMOTE_RESULT_DIR=/home/gtvchrome/results
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.atv_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverComplianceTestCase,CastReceiverMirroringProtocolTestCase,CastReceiverVolumeControlTestCase,CastReceiverMultipleSendersTestCase,CastReceiverMediaProtocolMP4TestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.android_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidTestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+CHROME_MIRRORING_APP_ID=0F5096E8
+
+# Test specific configuration values:
+MP4_MEDIA_URL=http://commondatastorage.googleapis.com/android-tv/Sample%20videos/Zeitgeist/Zeitgeist%202010_%20Year%20in%20Review.mp4
+MP4_MEDIA_MIME_TYPE=video/mp4
+DASH_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/dash/sample-dash-list.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+HLS_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/hls/bbb/bbb_hls.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+SMOOTH_STREAMING_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/smooth/BBB_H264_AACL/big_buck_bunny_1080p_stereo.ism/manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+MEDIA_LOAD_TIMEOUT_SECS=30
diff --git a/lab_system/master/config/tests/cast/atv_receiver_c2n_media_launch.cfg b/lab_system/master/config/tests/cast/atv_receiver_c2n_media_launch.cfg
new file mode 100644
index 0000000..1daaa42
--- /dev/null
+++ b/lab_system/master/config/tests/cast/atv_receiver_c2n_media_launch.cfg
@@ -0,0 +1,29 @@
+# Config file for Cast Connect media launch test on android devices
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.cast_connect_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastConnectMediaControlTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.android_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidTestSequence
+
+# Path and type of video for media launch test
+VIDEO_PATH=http://commondatastorage.googleapis.com/android-tv/Sample%20videos/April%20Fool%27s%202013/Introducing%20Google%20Nose.mp4
+VIDEO_TYPE=video/mp4
+VIDEO_5S_1920_1080=gs://home_media_server/media/static/samples/cast_connect_test_data/video_play_5s_1920x1080.png
+VIDEO_5S_720_576=gs://home_media_server/media/static/samples/cast_connect_test_data/video_play_5s_720x576.png
+
+# Gs url path of test demo app installed on android devices
+C2N_TEST_DEMO_APP_APK_URL=gs://android-build-artifacts/c2n-demo.apk
+
diff --git a/lab_system/master/config/tests/cast/atv_receiver_compliance.cfg b/lab_system/master/config/tests/cast/atv_receiver_compliance.cfg
new file mode 100644
index 0000000..b4d86a1
--- /dev/null
+++ b/lab_system/master/config/tests/cast/atv_receiver_compliance.cfg
@@ -0,0 +1,29 @@
+# Config file for the Cast receiver compliance tests running on Android TV
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Remote Test Setup
+USER_NAME=gtvchrome
+REMOTE_TEST_ROOT=/home/gtvchrome/repositories/HEAD
+REMOTE_RESULT_DIR=/home/gtvchrome/results
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.atv_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverComplianceTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.android_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidTestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+CHROME_MIRRORING_APP_ID=0F5096E8
diff --git a/lab_system/master/config/tests/cast/atv_receiver_mirroring.cfg b/lab_system/master/config/tests/cast/atv_receiver_mirroring.cfg
new file mode 100644
index 0000000..628d17b
--- /dev/null
+++ b/lab_system/master/config/tests/cast/atv_receiver_mirroring.cfg
@@ -0,0 +1,32 @@
+# Config file for the Cast receiver mirroring tests running on Android TV
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Remote Test Setup
+USER_NAME=gtvchrome
+REMOTE_TEST_ROOT=/home/gtvchrome/repositories/HEAD
+REMOTE_RESULT_DIR=/home/gtvchrome/results
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.atv_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverMirroringProtocolTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.android_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidTestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+CHROME_MIRRORING_APP_ID=0F5096E8
+
+# Test specific configuration values:
+MEDIA_LOAD_TIMEOUT_SECS=30
diff --git a/lab_system/master/config/tests/cast/atv_receiver_mp4_protocol.cfg b/lab_system/master/config/tests/cast/atv_receiver_mp4_protocol.cfg
new file mode 100644
index 0000000..2373259
--- /dev/null
+++ b/lab_system/master/config/tests/cast/atv_receiver_mp4_protocol.cfg
@@ -0,0 +1,34 @@
+# Config file for the Cast receiver media protocol MP4 tests running on Android TV
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Remote Test Setup
+USER_NAME=gtvchrome
+REMOTE_TEST_ROOT=/home/gtvchrome/repositories/HEAD
+REMOTE_RESULT_DIR=/home/gtvchrome/results
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.atv_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverMediaProtocolMP4TestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.android_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidTestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+CHROME_MIRRORING_APP_ID=0F5096E8
+
+# Test specific configuration values:
+MP4_MEDIA_URL=http://commondatastorage.googleapis.com/android-tv/Sample%20videos/Zeitgeist/Zeitgeist%202010_%20Year%20in%20Review.mp4
+MP4_MEDIA_MIME_TYPE=video/mp4
+MEDIA_LOAD_TIMEOUT_SECS=30
diff --git a/lab_system/master/config/tests/cast/atv_receiver_multi_senders.cfg b/lab_system/master/config/tests/cast/atv_receiver_multi_senders.cfg
new file mode 100644
index 0000000..fb14bca
--- /dev/null
+++ b/lab_system/master/config/tests/cast/atv_receiver_multi_senders.cfg
@@ -0,0 +1,29 @@
+# Config file for the Cast receiver multi-senders tests running on Android TV
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Remote Test Setup
+USER_NAME=gtvchrome
+REMOTE_TEST_ROOT=/home/gtvchrome/repositories/HEAD
+REMOTE_RESULT_DIR=/home/gtvchrome/results
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.atv_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverMultipleSendersTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.android_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidTestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+CHROME_MIRRORING_APP_ID=0F5096E8
diff --git a/lab_system/master/config/tests/cast/atv_receiver_volume.cfg b/lab_system/master/config/tests/cast/atv_receiver_volume.cfg
new file mode 100644
index 0000000..756e00f
--- /dev/null
+++ b/lab_system/master/config/tests/cast/atv_receiver_volume.cfg
@@ -0,0 +1,34 @@
+# Config file for the Cast receiver volume control tests running on Android TV
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Remote Test Setup
+USER_NAME=gtvchrome
+REMOTE_TEST_ROOT=/home/gtvchrome/repositories/HEAD
+REMOTE_RESULT_DIR=/home/gtvchrome/results
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.atv_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverVolumeControlTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.android_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidTestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+CHROME_MIRRORING_APP_ID=0F5096E8
+
+# Test specific configuration values:
+MP4_MEDIA_URL=http://commondatastorage.googleapis.com/android-tv/Sample%20videos/Zeitgeist/Zeitgeist%202010_%20Year%20in%20Review.mp4
+MP4_MEDIA_MIME_TYPE=video/mp4
+MEDIA_LOAD_TIMEOUT_SECS=30
diff --git a/lab_system/master/config/tests/cast/discovery.cfg b/lab_system/master/config/tests/cast/discovery.cfg
new file mode 100644
index 0000000..703c5c4
--- /dev/null
+++ b/lab_system/master/config/tests/cast/discovery.cfg
@@ -0,0 +1,69 @@
+# Configuration values for the Cast V2 mDNS test.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.discovery.receiver_discovery_mdns_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ReceiverDiscoveryMdnsTestCase
+
+# Name of the device type to host the server
+SERVER_DEVICE_TYPE=ubuntu-x86
+
+# Name of the device group to host the server
+SERVER_DEVICE_GROUP=ubuntu
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# These properties are required by CopyRepositoriesStepFactory step.
+# The properties should have had REMOTE_ prefix because the provided info
+# is for remote machine. Bug id 17896008.
+# TODO(khai): remove this block when the bug is fixed.
+SELF_IP=100.107.131.248
+USER_NAME=cctestdriver
+HOME_DIR=/home/cctestdriver
+ROOT_REPO=repository/eureka
+
+# IP address of the test driver
+REMOTE_IP=100.107.131.248
+
+# User name on the test driver
+REMOTE_USER_NAME=cctestdriver
+
+# Home directory of the user on the test driver
+REMOTE_HOME_DIR=/home/cctestdriver
+
+# Repository root on the test driver
+REMOTE_REPO_ROOT=repository/eureka
+
+# Directory for log files on the test driver
+REMOTE_LOG_DIR=/tmp/logs
+
+# Temporary directory on the test driver
+REMOTE_TMP_DIR=/tmp/temp
+
+# Test driver needs to know where to upload the execution results to. This is
+# the path test driver should use to point to correct db.
+REMOTE_SERVER_DB_CONFIG_PATH=/home/cctestdriver/repository/eureka/continuous-tests/db/db.config
+
+# Information of the test driver network interfaces. This must be in JSON
+# format {"ip_address_0": "iface_a", "ip_address_1": "iface_b", ...}.
+# Note:
+# 1) Keys and values must be strings using double quotes (") instead of single
+#    quotes because JSON parser doesn't understand them.
+# 2) Make sure there are no duplicate keys (i.e., same IP addresses) in the
+#    dictionary. The config file reader uses JSON parser for this property.
+#    If there are duplicate keys, the latter will override the previous ones.
+#    For example, {'127.0.0.1': 'localhost', '127.0.0.1': 'myhostname'} will
+#    result in {'127.0.0.1': 'myhostname'}.
+REMOTE_IFACES={"192.168.0.2": "eth2"}
+
+# Device under test, prefix must match the device_type in test_def.json
+# For example, if device_type in test_def.json is "lexx' then the device
+# IP is from LEXX_DEVICE_IP.
+LEXX_DEVICE_IP=192.168.0.100
diff --git a/lab_system/master/config/tests/cast/discovery/test_defs.json b/lab_system/master/config/tests/cast/discovery/test_defs.json
new file mode 100644
index 0000000..ce6b9f9
--- /dev/null
+++ b/lab_system/master/config/tests/cast/discovery/test_defs.json
@@ -0,0 +1,55 @@
+[
+    {
+        "branch": [
+            "master",
+            "release"
+        ],
+        "device_group": "lexx_eth",
+        "device_type": "lexx",
+        "group_name": "cast",
+        "hidden": "true",
+        "identifier": "discovery",
+        "slave_name": "discovery",
+        "step_factories": [
+            {
+                "step_description": "Allocates devices for the testrun",
+                "step_factories": [
+                    {
+                        "class_name": "AllocateTestDriverAndDutStepFactory",
+                        "module_path": "lab_system.master.steps.factory.device.cloud_allocator"
+                    }
+                ],
+                "step_identifier": "device_allocation"
+            },
+            {
+                "step_description": "Copying build to test slave and installing on device",
+                "step_factories": [
+                    {
+                        "class_name": "CopyRepositoriesStepFactory",
+                        "module_path": "lab_system.master.steps.factory.build.copy_test_repo"
+                    },
+                    {
+                        "class_name": "BuildFetcherStepFactory",
+                        "module_path": "lab_system.master.steps.factory.build.fetcher"
+                    },
+                    {
+                        "class_name": "TestDriverBuildInstallerStepFactory",
+                        "module_path": "lab_system.master.steps.factory.build.installer_eurlab"
+                    }
+                ],
+                "step_identifier": "build_handling"
+            },
+            {
+                "step_description": "Executes test_runner on hard-coded remote workstation.",
+                "step_factories": [
+                    {
+                        "class_name": "ReceiverDiscoveryRemoteRunner",
+                        "module_path": "lab_system.master.steps.factory.run.receiver_discovery_remote_runner"
+                    }
+                ],
+                "step_identifier": "test_running"
+            }
+        ],
+        "test_name": "discovery"
+    }
+]
diff --git a/lab_system/master/config/tests/cast/fuchsia_receiver.cfg b/lab_system/master/config/tests/cast/fuchsia_receiver.cfg
new file mode 100644
index 0000000..4ae4f65
--- /dev/null
+++ b/lab_system/master/config/tests/cast/fuchsia_receiver.cfg
@@ -0,0 +1,44 @@
+# Config file for the Cast receiver tests on fuchsia devices
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.fuchsia_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverComplianceTestCase,CastReceiverMultipleSendersTestCase,CastReceiverMirroringProtocolTestCase,CastReceiverVolumeControlTestCase,CastReceiverStressTestCase,CastReceiverMediaProtocolMP4TestCase,CastReceiverMediaProtocolDASHTestCase,CastReceiverMediaProtocolHLSTestCase,CastReceiverMediaProtocolSmoothStreamingTestCase,CastReceiverBenchmarkTestCase,CastReceiverQueueTestCase,CastReceiverCapabilityTestCase
+
+# BUG(b/171057532): OOBE required to bypass kronk for cast tests.
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.fuchsia_oobe_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaOobeTestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+CHROME_MIRRORING_APP_ID=0F5096E8
+
+# Test specific configuration values:
+MP4_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/mp4/bbb_1080p_30fps_x264_high_41_mp3.mp4
+MP4_MEDIA_MIME_TYPE=video/mp4
+DASH_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/dash/sample-dash-list.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+HLS_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/hls/bbb/bbb_hls.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+SMOOTH_STREAMING_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/smooth/BBB_H264_AACL/big_buck_bunny_1080p_stereo.ism/manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+MEDIA_LOAD_TIMEOUT_SECS=30
+
+# This config is used to determine if cast operations should be allowed even if
+# device is not signed-in using a gaia account.
+ENABLE_CAST_WITHOUT_SIGN_IN=True
+
+# Whether or not the list of test classes should be filtered. This is to be able to pass
+# the extraction_path modifier.
+SHOULD_NOT_FILTER=True
diff --git a/lab_system/master/config/tests/cast/mdns.cfg b/lab_system/master/config/tests/cast/mdns.cfg
new file mode 100644
index 0000000..dd4a557
--- /dev/null
+++ b/lab_system/master/config/tests/cast/mdns.cfg
@@ -0,0 +1,34 @@
+# Configuration values for the Cast V2 mDNS test.
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.mdns.mdns_remote_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastMdnsRemoteTestCase
+
+# Name of the device type to host the server
+SERVER_DEVICE_TYPE=ubuntu-x86
+
+# Name of the device group to host the server
+SERVER_DEVICE_GROUP=DEFAULT
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# must be in-sync with
+# continuous-test/lab_system/master/config/groups/cast_mdns.cfg
+REMOTE_SERVER_IP=172.17.23.200
+REMOTE_SERVER_USERNAME=gtvchrome
+
+# path that remote server should use to point to db.config
+REMOTE_SERVER_DB_CONFIG_PATH=/home/gtvchrome/repository/eureka/continuous-tests/db/db.config
+
+# PROPERTIES
+SELF_IP=172.17.23.200
+HOME_DIR=/home/gtvchrome
+USER_NAME=gtvchrome
+REPO_ROOT=repository/eureka
+
diff --git a/lab_system/master/config/tests/cast/mdns/test_defs.json b/lab_system/master/config/tests/cast/mdns/test_defs.json
new file mode 100644
index 0000000..ab93a5e
--- /dev/null
+++ b/lab_system/master/config/tests/cast/mdns/test_defs.json
@@ -0,0 +1,42 @@
+[
+  {
+    "group_name": "cast",
+    "slave_name": "cast_mdns",
+    "test_name": "mdns",
+    "identifier": "cast_mdns",
+    "device_type": "eureka",
+    "device_group": "eureka_debug_23",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "step_factories": [
+      {
+        "step_description": "Copying Build to test slave and installing on device",
+        "step_identifier": "build_handling",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.build.copy_test_repo",
+            "class_name": "CopyRepositoriesStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.build.fetcher",
+            "class_name": "BuildFetcherStepFactory"
+          },
+          {
+            "module_path": "lab_system.master.steps.factory.build.installer_eurlab",
+            "class_name": "BuildInstallerStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Executes test_runner on hard-coded remote workstation.",
+        "step_identifier": "test_running",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.cast_mdns_remote_runner",
+            "class_name": "CastMdnsRemoteRunnerHack"
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/tests/cast/movies.cfg b/lab_system/master/config/tests/cast/movies.cfg
new file mode 100644
index 0000000..15677ce
--- /dev/null
+++ b/lab_system/master/config/tests/cast/movies.cfg
@@ -0,0 +1,28 @@
+# Config file for the Google Play Movies cast tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.apps.movies.play_movies_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PlayMoviesTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+
+# oauth credential file location. The default test account.
+OAUTH_CREDENTIAL_FILE=~/.config/google-avi/default.oauth2.dat
+# oauth credential file location. The test account for Multi-player testing.
+OAUTH_CREDENTIAL_FILE_SECOND=~/.config/google-avi/multi_player.oauth2.dat
diff --git a/lab_system/master/config/tests/cast/movies/test_defs.json b/lab_system/master/config/tests/cast/movies/test_defs.json
new file mode 100644
index 0000000..e59d0ee
--- /dev/null
+++ b/lab_system/master/config/tests/cast/movies/test_defs.json
@@ -0,0 +1,22 @@
+[
+  {
+    "group_name": "cast",
+    "slave_name": "play_movies",
+    "test_name": "movies",
+    "identifier": "play_movies",
+    "device_type": "eureka",
+    "device_group": "play_movie_app_test",
+    "hidden": "true",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "cast",
+    "slave_name": "play_movies_v2",
+    "test_name": "movies",
+    "identifier": "play_movies_v2",
+    "device_type": "lexx",
+    "device_group": "play_movie_app_test",
+    "hidden": "true",
+    "branch": ["master", "release"]
+  }
+]
diff --git a/lab_system/master/config/tests/cast/opencast.cfg b/lab_system/master/config/tests/cast/opencast.cfg
new file mode 100644
index 0000000..93bb9d0
--- /dev/null
+++ b/lab_system/master/config/tests/cast/opencast.cfg
@@ -0,0 +1,32 @@
+# Config file for the Opencast receiver tests
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.opencast_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpencastReceiverComplianceTestCase,OpencastMultipleSendersTestCase,OpencastReceiverBenchmarkTestCase,OpencastReceiverMediaProtocolTestCase,OpencastReceiverStressTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.cast.receiver.opencast_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OpencastTestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+
+# Test specific configuration values:
+MP4_MEDIA_URL=https://home-media-server.sandbox.google.com/static/media/car-1920x1080.mp4
+MP4_MEDIA_MIME_TYPE=video/mp4
+DASH_MEDIA_URL=https://home-media-server.sandbox.google.com/static/tests/js_mse_eme/media/car-20120827-manifest.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+HLS_MEDIA_URL=https://home-media-server.sandbox.google.com/static/dumps/media/hls/unicornmedia/demo.umedia.com/cast/testfiles/preroll_midroll60s_postroll.zip/manifest.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+SMOOTH_STREAMING_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/smooth/BBB_H264_AACL/big_buck_bunny_1080p_stereo.ism/manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+MEDIA_LOAD_TIMEOUT_SECS=30
+
+
diff --git a/lab_system/master/config/tests/cast/opencast/test_defs.json b/lab_system/master/config/tests/cast/opencast/test_defs.json
new file mode 100644
index 0000000..aa72851
--- /dev/null
+++ b/lab_system/master/config/tests/cast/opencast/test_defs.json
@@ -0,0 +1,57 @@
+[
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_opencast",
+    "test_name": "opencast",
+    "identifier": "opencast_receiver",
+    "device_type": "eureka",
+    "device_group": "hdmi_plugged_with_cec_100",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_opencast",
+    "test_name": "opencast",
+    "identifier": "opencast_receiver_v2",
+    "device_type": "lexx",
+    "device_group": "hdmi_plugged_with_cec_100",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_opencast",
+    "test_name": "opencast",
+    "identifier": "opencast_receiver_v3",
+    "device_type": "earth",
+    "device_group": "hdmi_plugged_with_cec_100",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_opencast",
+    "test_name": "opencast_c4a",
+    "identifier": "opencast_receiver_hendrix",
+    "device_type": "hendrix",
+    "device_group": "hendrix_eth",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_opencast",
+    "test_name": "opencast",
+    "identifier": "opencast_receiver_v4",
+    "device_type": "luther_b2",
+    "device_group": "luther_eth",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_opencast",
+    "test_name": "opencast_c4a",
+    "identifier": "opencast_receiver_chirp",
+    "device_type": "chirp",
+    "device_group": "chirp",
+    "build_flavor": "user",
+    "branch": ["master", "release"]
+  }
+]
diff --git a/lab_system/master/config/tests/cast/opencast_c4a.cfg b/lab_system/master/config/tests/cast/opencast_c4a.cfg
new file mode 100644
index 0000000..922515f
--- /dev/null
+++ b/lab_system/master/config/tests/cast/opencast_c4a.cfg
@@ -0,0 +1,41 @@
+# Config file for the Opencast receiver tests
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.opencast_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpencastReceiverComplianceTestCase,OpencastMultipleSendersTestCase,OpencastReceiverBenchmarkTestCase,OpencastReceiverMediaProtocolTestCase,OpencastReceiverStressTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.cast.receiver.opencast_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OpencastTestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+
+# Test specific configuration values:
+MP4_MEDIA_URL=https://storage.googleapis.com/cast_media/format_tests/audio/stereo_guitar.mp3
+MP4_MEDIA_MIME_TYPE=audio/mp3
+
+DASH_MEDIA_URL=https://home-media-server.sandbox.google.com/static/formats/audio/stereo_guitar.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+
+HLS_MEDIA_URL=https://home-media-server.sandbox.google.com/static/formats/audio/hls_aac_he/stereo_guitar.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+
+SMOOTH_STREAMING_MEDIA_URL=https://home-media-server.sandbox.google.com/static/formats/audio/stereo_guitar.ism/Manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+
+MEDIA_LOAD_TIMEOUT_SECS=30
+
+# Flag to skip tests that require a display and can't be run in C4A devices.
+NO_DISPLAY=1
+
+# This config is used to determine if cast operations should be allowed even if
+# device is not signed-in using a gaia account.
+ENABLE_CAST_WITHOUT_SIGN_IN=True
diff --git a/lab_system/master/config/tests/cast/receiver.cfg b/lab_system/master/config/tests/cast/receiver.cfg
new file mode 100644
index 0000000..7caf323
--- /dev/null
+++ b/lab_system/master/config/tests/cast/receiver.cfg
@@ -0,0 +1,41 @@
+# Config file for the Cast receiver tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.compliance_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverComplianceTestCase,CastReceiverMultipleSendersTestCase,CastReceiverMirroringProtocolTestCase,CastReceiverVolumeControlTestCase,CastReceiverStressTestCase,CastReceiverMediaProtocolMP4TestCase,CastReceiverMediaProtocolDASHTestCase,CastReceiverMediaProtocolHLSTestCase,CastReceiverMediaProtocolSmoothStreamingTestCase,CastReceiverBenchmarkTestCase,CastReceiverQueueTestCase,CastReceiverCapabilityTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+CHROME_MIRRORING_APP_ID=0F5096E8
+
+# Test specific configuration values:
+MP4_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/mp4/bbb_1080p_30fps_x264_high_41_mp3.mp4
+MP4_MEDIA_MIME_TYPE=video/mp4
+DASH_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/dash/sample-dash-list.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+HLS_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/hls/bbb/bbb_hls.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+SMOOTH_STREAMING_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/smooth/BBB_H264_AACL/big_buck_bunny_1080p_stereo.ism/manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+MEDIA_LOAD_TIMEOUT_SECS=30
+
+# This config is used to determine if cast operations should be allowed even if
+# device is not signed-in using a gaia account.
+ENABLE_CAST_WITHOUT_SIGN_IN=True
+
+
diff --git a/lab_system/master/config/tests/cast/receiver/test_defs.json b/lab_system/master/config/tests/cast/receiver/test_defs.json
new file mode 100644
index 0000000..95f330c
--- /dev/null
+++ b/lab_system/master/config/tests/cast/receiver/test_defs.json
@@ -0,0 +1,83 @@
+[
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_cast",
+    "test_name": "receiver_c4a_master_vol",
+    "identifier": "cast_receiver_assistantdefault",
+    "device_type": "assistantdefault",
+    "device_group": "oem_assistant",
+    "branch": ["1_26a", "master", "release", "next_release"]
+  },
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_cast",
+    "test_name": "receiver_tv_master_vol",
+    "identifier": "cast_receiver_compliance_bubbletea",
+    "device_type": "bubbletea",
+    "device_group": "bubbletea",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  },
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_cast",
+    "test_name": "receiver_tv_master_vol",
+    "identifier": "cast_receiver_compliance_entourage",
+    "device_type": "dragonfly",
+    "device_group": "entourage",
+    "branch": ["cast_tv_dragonfly_master", "cast_tv_dragonfly_release", "cast_tv_dragonfly_next_release"]
+  },
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_cast",
+    "test_name": "receiver_tv_master_vol",
+    "identifier": "cast_receiver_compliance_guabao",
+    "device_type": "guabao",
+    "device_group": "entourage",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  },
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_cast",
+    "test_name": "receiver_tv_master_vol",
+    "identifier": "cast_receiver_compliance_mars_us",
+    "device_type": "Mars_US",
+    "device_group": "entourage",
+    "branch": ["cast_tv_mediatek_master", "cast_tv_mediatek_release", "cast_tv_mediatek_next_release"]
+  },
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_cast",
+    "test_name": "receiver_tv_master_vol",
+    "identifier": "cast_receiver_compliance_N00007",
+    "device_type": "N00007",
+    "device_group": "entourage",
+    "branch": ["cast_tv_novatek_master", "cast_tv_novatek_release", "cast_tv_novatek_next_release"]
+  },
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_cast",
+    "test_name": "receiver_tv_master_vol",
+    "identifier": "cast_receiver_compliance_suncake",
+    "device_type": "suncake",
+    "device_group": "entourage",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  },
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_cast",
+    "test_name": "receiver_tv_master_vol",
+    "identifier": "cast_receiver_compliance_redwood",
+    "device_type": "redwood",
+    "device_group": "redwood",
+    "branch": ["cast_tv_marvell_master", "cast_tv_marvell_release", "cast_tv_marvell_next_release"]
+  },
+  {
+    "group_name": "cast",
+    "slave_name": "eurtest_cast",
+    "test_name": "receiver_tv_master_vol",
+    "identifier": "cast_receiver_compliance_taroball",
+    "device_type": "taroball",
+    "device_group": "entourage",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  }
+]
diff --git a/lab_system/master/config/tests/cast/receiver_c4a.cfg b/lab_system/master/config/tests/cast/receiver_c4a.cfg
new file mode 100644
index 0000000..7878c60
--- /dev/null
+++ b/lab_system/master/config/tests/cast/receiver_c4a.cfg
@@ -0,0 +1,47 @@
+# Config file for the Cast receiver tests on audio devices
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.compliance_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverComplianceTestCase,CastReceiverMultipleSendersTestCase,CastReceiverMirroringProtocolTestCase,CastReceiverVolumeControlTestCase,CastReceiverStressTestCase,CastReceiverMediaProtocolMP4TestCase,CastReceiverMediaProtocolSmoothStreamingTestCase,CastReceiverBenchmarkTestCase,CastReceiverQueueTestCase,CastReceiverCapabilityTestCase,CastReceiverMediaProtocolHLSTestCase,CastReceiverMediaProtocolDASHTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+CHROME_MIRRORING_APP_ID=85CDB22F
+
+# Test specific configuration values:
+MP4_MEDIA_URL=https://storage.googleapis.com/cast_media/format_tests/audio/stereo_guitar.mp3
+MP4_MEDIA_MIME_TYPE=video/mp4
+
+# Temporary fix
+DASH_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/dash/sample-dash-list.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+HLS_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/hls/bbb/bbb_hls.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+SMOOTH_STREAMING_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/smooth/BBB_H264_AACL/big_buck_bunny_1080p_stereo.ism/manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+
+MEDIA_LOAD_TIMEOUT_SECS=30
+
+# Flag to skip tests that require a display and can't be run in C4A devices.
+NO_DISPLAY=1
+
+# This config is used to determine if cast operations should be allowed even if
+# device is not signed-in using a gaia account.
+ENABLE_CAST_WITHOUT_SIGN_IN=True
+
+
diff --git a/lab_system/master/config/tests/cast/receiver_c4a_master_vol.cfg b/lab_system/master/config/tests/cast/receiver_c4a_master_vol.cfg
new file mode 100644
index 0000000..9ee2c71
--- /dev/null
+++ b/lab_system/master/config/tests/cast/receiver_c4a_master_vol.cfg
@@ -0,0 +1,49 @@
+# Config file for the Cast receiver tests on audio devices with
+# master volume control
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.compliance_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverComplianceTestCase,CastReceiverMultipleSendersTestCase,CastReceiverMirroringProtocolTestCase,CastReceiverVolumeControlTestCase,CastReceiverStressTestCase,CastReceiverMediaProtocolMP4TestCase,CastReceiverMediaProtocolDASHTestCase,CastReceiverMediaProtocolHLSTestCase,CastReceiverMediaProtocolSmoothStreamingTestCase,CastReceiverBenchmarkTestCase,CastReceiverQueueTestCase,CastReceiverCapabilityTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+
+# Test specific configuration values:
+MP4_MEDIA_URL=https://storage.googleapis.com/cast_media/format_tests/audio/stereo_guitar.mp3
+MP4_MEDIA_MIME_TYPE=video/mp4
+
+DASH_MEDIA_URL=https://home-media-server.sandbox.google.com/static/formats/audio/stereo_guitar.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+
+HLS_MEDIA_URL=https://home-media-server.sandbox.google.com/static/formats/audio/hls_aac_he/stereo_guitar.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+
+SMOOTH_STREAMING_MEDIA_URL=https://home-media-server.sandbox.google.com/static/formats/audio/stereo_guitar.ism/Manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+
+MEDIA_LOAD_TIMEOUT_SECS=30
+
+# Flag to skip tests that require a display and can't be run in C4A devices.
+NO_DISPLAY=1
+
+# This config is used to determine if cast operations should be allowed even if
+# device is not signed-in using a gaia account.
+ENABLE_CAST_WITHOUT_SIGN_IN=True
+
+# Master volume control
+ANALOG_VOLUME=1
diff --git a/lab_system/master/config/tests/cast/receiver_dogfood.cfg b/lab_system/master/config/tests/cast/receiver_dogfood.cfg
new file mode 100644
index 0000000..c43ad53
--- /dev/null
+++ b/lab_system/master/config/tests/cast/receiver_dogfood.cfg
@@ -0,0 +1,38 @@
+# Config file for the dogfood Cast receiver tests
+#
+# These tests run on dogfood versions of the Cast
+# Media Receiver, Media Player Library and Test
+# Cast Client Receiver.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.compliance_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverComplianceTestCase,CastReceiverMultipleSendersTestCase,CastReceiverMirroringProtocolTestCase,CastReceiverVolumeControlTestCase,CastReceiverStressTestCase,CastReceiverMediaProtocolMP4TestCase,CastReceiverMediaProtocolDASHTestCase,CastReceiverMediaProtocolHLSTestCase,CastReceiverMediaProtocolSmoothStreamingTestCase,CastReceiverBenchmarkTestCase,CastReceiverQueueTestCase,CastReceiverCapabilityTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=F83E3215
+CAST_V2_PLAYER_APP_ID=9B04B019
+
+# Test specific configuration values:
+MP4_MEDIA_URL=https://home-media-server.sandbox.google.com/static/media/car-1920x1080.mp4
+MP4_MEDIA_MIME_TYPE=video/mp4
+DASH_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/dash/sample-dash-list.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+HLS_MEDIA_URL=https://home-media-server.sandbox.google.com/static/dumps/media/hls/unicornmedia/demo.umedia.com/cast/testfiles/preroll_midroll60s_postroll.zip/manifest.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+SMOOTH_STREAMING_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/smooth/BBB_H264_AACL/big_buck_bunny_1080p_stereo.ism/manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+MEDIA_LOAD_TIMEOUT_SECS=30
diff --git a/lab_system/master/config/tests/cast/receiver_tv_master_vol.cfg b/lab_system/master/config/tests/cast/receiver_tv_master_vol.cfg
new file mode 100644
index 0000000..e926757
--- /dev/null
+++ b/lab_system/master/config/tests/cast/receiver_tv_master_vol.cfg
@@ -0,0 +1,38 @@
+# Config file for the Cast receiver tests for TVs with
+# master volume control
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.compliance_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverComplianceTestCase,CastReceiverMultipleSendersTestCase,CastReceiverMirroringProtocolTestCase,CastReceiverVolumeControlTestCase,CastReceiverStressTestCase,CastReceiverMediaProtocolMP4TestCase,CastReceiverMediaProtocolDASHTestCase,CastReceiverMediaProtocolHLSTestCase,CastReceiverMediaProtocolSmoothStreamingTestCase,CastReceiverBenchmarkTestCase,CastReceiverQueueTestCase,CastReceiverCapabilityTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+
+# Test specific configuration values:
+MP4_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/mp4/bbb_1080p_30fps_x264_high_41_mp3.mp4
+MP4_MEDIA_MIME_TYPE=video/mp4
+DASH_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/dash/sample-dash-list.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+HLS_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/hls/bbb/bbb_hls.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+SMOOTH_STREAMING_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/smooth/BBB_H264_AACL/big_buck_bunny_1080p_stereo.ism/manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+MEDIA_LOAD_TIMEOUT_SECS=30
+
+# Master volume control
+ANALOG_VOLUME=1
diff --git a/lab_system/master/config/tests/cast/receiver_x86.cfg b/lab_system/master/config/tests/cast/receiver_x86.cfg
new file mode 100644
index 0000000..4b3f724
--- /dev/null
+++ b/lab_system/master/config/tests/cast/receiver_x86.cfg
@@ -0,0 +1,41 @@
+# Config file to run the Cast receiver tests against a local x86 cast shell.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.compliance_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverComplianceTestCase,CastReceiverMultipleSendersTestCase,CastReceiverMirroringProtocolTestCase,CastReceiverVolumeControlTestCase,CastReceiverStressTestCase,CastReceiverMediaProtocolMP4TestCase,CastReceiverMediaProtocolDASHTestCase,CastReceiverMediaProtocolHLSTestCase,CastReceiverMediaProtocolSmoothStreamingTestCase,CastReceiverBenchmarkTestCase,CastReceiverQueueTestCase,CastReceiverCapabilityTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=X86DisabledLoggingTestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+
+# Test specific configuration values:
+MP4_MEDIA_URL=https://storage.googleapis.com/cast_media/format_tests/video/bbb_1080p_30fps_x264_high_41_mp3.mp4
+MP4_MEDIA_MIME_TYPE=video/mp4
+DASH_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/dash/sample-dash-list.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+HLS_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/hls/bbb/bbb_hls.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+SMOOTH_STREAMING_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/smooth/BBB_H264_AACL/big_buck_bunny_1080p_stereo.ism/manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+MEDIA_LOAD_TIMEOUT_SECS=30
+ANALOG_VOLUME=1
+
+# Tests that are not applicable for x86 cast shell (b/25285537)
+TESTS_TO_SKIP=CastReceiverComplianceTestCase.testIsActiveInput
+
+# Skip SSH communication
+SKIP_SSH=True
diff --git a/lab_system/master/config/tests/cast_androidthings/cast_androidthings.cfg b/lab_system/master/config/tests/cast_androidthings/cast_androidthings.cfg
new file mode 100644
index 0000000..cb270ba
--- /dev/null
+++ b/lab_system/master/config/tests/cast_androidthings/cast_androidthings.cfg
@@ -0,0 +1,53 @@
+# Config file for Cast receiver tests on Android Things devices.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Remote Test Setup
+USER_NAME=gtvchrome
+REMOTE_TEST_ROOT=/home/gtvchrome/repositories/HEAD
+REMOTE_RESULT_DIR=/home/gtvchrome/results
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.athings_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverComplianceTestCase,CastReceiverCapabilityTestCase,CastReceiverMultipleSendersTestCase,CastReceiverVolumeControlTestCase,CastReceiverMediaProtocolMP4TestCase,CastReceiverMediaProtocolHLSTestCase,CastReceiverMediaProtocolDASHTestCase,CastReceiverMediaProtocolSmoothStreamingTestCase,CastReceiverStressTestCase,CastReceiverBenchmarkTestCase,CastReceiverQueueTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.cast.receiver.athings_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Default ports on receiver
+DEFAULT_HTTP_PORT=8008
+DEFAULT_RECEIVER_PORT=8009
+DEFAULT_DEBUG_PORT=9222
+USE_ADB_FORWARD=True
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+
+# Test specific configuration values:
+MP4_MEDIA_URL=https://storage.googleapis.com/cast_media/format_tests/audio/stereo_guitar.mp3
+MP4_MEDIA_MIME_TYPE=video/mp4
+DASH_MEDIA_URL=https://storage.googleapis.com/cast_media/format_tests/audio/stereo_guitar.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+HLS_MEDIA_URL=https://storage.googleapis.com/cast_media/format_tests/audio/hls_aac_he/stereo_guitar.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+SMOOTH_STREAMING_MEDIA_URL=https://storage.googleapis.com/cast_media/format_tests/audio/stereo_guitar.ism/Manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+
+MEDIA_LOAD_TIMEOUT_SECS=30
+
+# Flag to skip tests that require a display and can't be run in C4A devices.
+NO_DISPLAY=1
+
+# # This config is used to determine if cast operations should be allowed even if
+# # device is not signed-in using a gaia account.
+ENABLE_CAST_WITHOUT_SIGN_IN=True
diff --git a/lab_system/master/config/tests/cast_androidthings/cast_androidthings_cq_presubmit.cfg b/lab_system/master/config/tests/cast_androidthings/cast_androidthings_cq_presubmit.cfg
new file mode 100644
index 0000000..0af2771
--- /dev/null
+++ b/lab_system/master/config/tests/cast_androidthings/cast_androidthings_cq_presubmit.cfg
@@ -0,0 +1,49 @@
+# Config file for the Cast receiver tests running on AndroidThings.
+#
+# This config file is used in CQ presubmit.
+#
+# PROPERTIES
+
+# Remote Test Setup
+USER_NAME=gtvchrome
+REMOTE_TEST_ROOT=/home/gtvchrome/repositories/HEAD
+REMOTE_RESULT_DIR=/home/gtvchrome/results
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.athings_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverComplianceTestCase,CastReceiverCapabilityTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.cast.receiver.athings_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Default ports on receiver
+DEFAULT_HTTP_PORT=8008
+DEFAULT_RECEIVER_PORT=8009
+DEFAULT_DEBUG_PORT=9222
+USE_ADB_FORWARD=True
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+
+# Test specific configuration values:
+MP4_MEDIA_URL=https://storage.googleapis.com/cast_media/format_tests/audio/stereo_guitar.mp3
+MP4_MEDIA_MIME_TYPE=video/mp4
+DASH_MEDIA_URL=https://storage.googleapis.com/cast_media/format_tests/audio/stereo_guitar.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+HLS_MEDIA_URL=https://storage.googleapis.com/cast_media/format_tests/audio/hls_aac_he/stereo_guitar.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+SMOOTH_STREAMING_MEDIA_URL=https://storage.googleapis.com/cast_media/format_tests/audio/stereo_guitar.ism/Manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+
+# Flag to skip tests that require a display and can't be run in C4A devices.
+NO_DISPLAY=1
+
+# # This config is used to determine if cast operations should be allowed even if
+# # device is not signed-in using a gaia account.
+ENABLE_CAST_WITHOUT_SIGN_IN=True
diff --git a/lab_system/master/config/tests/cast_androidthings/test_defs.json b/lab_system/master/config/tests/cast_androidthings/test_defs.json
new file mode 100644
index 0000000..e0d80ee
--- /dev/null
+++ b/lab_system/master/config/tests/cast_androidthings/test_defs.json
@@ -0,0 +1,12 @@
+[
+  {
+    "group_name": "cast_androidthings",
+    "slave_name": "androidthings_receiver",
+    "test_name": "androidthings_receiver",
+    "identifier": "androidthings_receiver",
+    "device_type": "imx7d_pico",
+    "device_group": "unit_test",
+    "branch": ["master", "androidthings_release", "androidthings_next_release"],
+    "step_factories_recipe": "step_factories_recipes/androidthings_cast_test_recipe.json"
+  }
+]
diff --git a/lab_system/master/config/tests/cast_atv/test_defs.json b/lab_system/master/config/tests/cast_atv/test_defs.json
new file mode 100644
index 0000000..54bbf48
--- /dev/null
+++ b/lab_system/master/config/tests/cast_atv/test_defs.json
@@ -0,0 +1,22 @@
+[
+  {
+    "group_name": "cast_atv",
+    "slave_name": "atv_receiver",
+    "test_name": "atv_receiver",
+    "identifier": "atv_receiver",
+    "device_type": "fugu",
+    "device_group": "cast_fugu",
+    "branch": ["master", "release", "next_release"],
+    "step_factories_recipe": "step_factories_recipes/atv_cast_test_recipe.json"
+  },
+  {
+    "group_name": "cast_atv",
+    "slave_name": "atv_receiver_shieldtv",
+    "test_name": "atv_receiver",
+    "identifier": "atv_receiver_shieldtv",
+    "device_type": "darcy",
+    "device_group": "unit_test",
+    "branch": ["master", "release", "next_release"],
+    "step_factories_recipe": "step_factories_recipes/atv_cast_test_recipe.json"
+  }
+]
diff --git a/lab_system/master/config/tests/cast_tv_unit_test/test_defs.json b/lab_system/master/config/tests/cast_tv_unit_test/test_defs.json
new file mode 100644
index 0000000..18d86f5
--- /dev/null
+++ b/lab_system/master/config/tests/cast_tv_unit_test/test_defs.json
@@ -0,0 +1,114 @@
+[
+    {
+        "branch": ["cast_tv_dragonfly_master", "cast_tv_dragonfly_release", "cast_tv_dragonfly_next_release"],
+        "build_flavor": "eng",
+        "device_group": "unit_test",
+        "device_type": "dragonfly",
+        "identifier": "dragonfly_eng",
+        "scheduler": "*/10 * * * *",
+        "slave_name": "dragonfly_eng",
+        "slave_number": [
+            1,
+            2
+        ],
+        "step_factories_recipe": "step_factories_recipes/unit_test_no_usb_check_recipe.json"
+    },
+    {
+        "branch": ["cast_tv_marvell_master", "cast_tv_marvell_release", "cast_tv_marvell_next_release"],
+        "build_flavor": "eng",
+        "device_group": "unit_test",
+        "device_type": "redwood",
+        "identifier": "redwood_eng",
+        "scheduler": "*/10 * * * *",
+        "slave_name": "redwood_eng",
+        "slave_number": [
+            1,
+            2
+        ],
+        "step_factories_recipe": "step_factories_recipes/unit_test_no_usb_check_recipe.json"
+    },
+    {
+        "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"],
+        "build_flavor": "eng",
+        "device_group": "unit_test",
+        "device_type": "bubbletea",
+        "identifier": "bubbletea_eng",
+        "scheduler": "*/10 * * * *",
+        "slave_name": "bubbletea_eng",
+        "slave_number": [
+            1,
+            2
+        ],
+        "step_factories_recipe": "step_factories_recipes/claim_hint_unit_test_recipe.json"
+    },
+    {
+        "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"],
+        "build_flavor": "eng",
+        "device_group": "unit_test",
+        "device_type": "guabao",
+        "identifier": "guabao_eng",
+        "scheduler": "*/10 * * * *",
+        "slave_name": "guabao_eng",
+        "slave_number": [
+            1,
+            2
+        ],
+        "step_factories_recipe": "step_factories_recipes/claim_hint_unit_test_recipe.json"
+    },
+    {
+        "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"],
+        "build_flavor": "eng",
+        "device_group": "unit_test",
+        "device_type": "suncake",
+        "identifier": "suncake_eng",
+        "scheduler": "*/10 * * * *",
+        "slave_name": "suncake_eng",
+        "slave_number": [
+            1,
+            2
+        ],
+        "step_factories_recipe": "step_factories_recipes/claim_hint_unit_test_recipe.json"
+    },
+    {
+        "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"],
+        "build_flavor": "eng",
+        "device_group": "unit_test",
+        "device_type": "taroball",
+        "identifier": "taroball_eng",
+        "scheduler": "*/10 * * * *",
+        "slave_name": "taroball_eng",
+        "slave_number": [
+            1,
+            2
+        ],
+        "step_factories_recipe": "step_factories_recipes/claim_hint_unit_test_recipe.json"
+    },
+    {
+        "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"],
+        "build_flavor": "eng",
+        "device_group": "unit_test",
+        "device_type": "Mars_US",
+        "identifier": "Mars_US_eng",
+        "scheduler": "*/10 * * * *",
+        "slave_name": "Mars_US_eng",
+        "slave_number": [
+            1,
+            2
+        ],
+        "step_factories_recipe": "step_factories_recipes/unit_test_lower_case_product_name_recipe.json"
+    },
+    {
+        "branch": ["cast_tv_novatek_master", "cast_tv_novatek_release", "cast_tv_novatek_next_release"],
+        "build_flavor": "eng",
+        "device_group": "unit_test",
+        "device_type": "N00007",
+        "identifier": "N00007_eng",
+        "scheduler": "*/10 * * * *",
+        "slave_name": "N00007_eng",
+        "slave_number": [
+            1,
+            2
+        ],
+        "step_factories_recipe": "step_factories_recipes/unit_test_lower_case_product_name_recipe.json"
+    }
+]
diff --git a/lab_system/master/config/tests/cast_video/video_decode_accelerator.cfg b/lab_system/master/config/tests/cast_video/video_decode_accelerator.cfg
new file mode 100644
index 0000000..95895c6
--- /dev/null
+++ b/lab_system/master/config/tests/cast_video/video_decode_accelerator.cfg
@@ -0,0 +1,16 @@
+# Config file for video decode accelerator tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast_video.video_decode_accelerator_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=VideoDecodeAcceleratorTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.cast_video.video_decode_accelerator_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=VideoDecodeAcceleratorTestSequence
+
diff --git a/lab_system/master/config/tests/cast_video/video_encode_accelerator.cfg b/lab_system/master/config/tests/cast_video/video_encode_accelerator.cfg
new file mode 100644
index 0000000..78ea1f3
--- /dev/null
+++ b/lab_system/master/config/tests/cast_video/video_encode_accelerator.cfg
@@ -0,0 +1,16 @@
+# Config file for video encode accelerator tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast_video.video_encode_accelerator_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=VideoEncodeAcceleratorTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.cast_video.video_encode_accelerator_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=VideoEncodeAcceleratorTestSequence
+
diff --git a/lab_system/master/config/tests/check_logs/test_check_logs.cfg b/lab_system/master/config/tests/check_logs/test_check_logs.cfg
new file mode 100644
index 0000000..a5ceb21
--- /dev/null
+++ b/lab_system/master/config/tests/check_logs/test_check_logs.cfg
@@ -0,0 +1,14 @@
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.locate_string.simple_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=GetBadBlocks
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.locate_string.locate_string_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CheckLogsTestSequence
+
+# Set to true to disable check logs:
+# DISABLE_CHECK_LOGS_FOR_STRINGS=True
diff --git a/lab_system/master/config/tests/cloudcast/cloudcast.cfg b/lab_system/master/config/tests/cloudcast/cloudcast.cfg
new file mode 100644
index 0000000..9786038
--- /dev/null
+++ b/lab_system/master/config/tests/cloudcast/cloudcast.cfg
@@ -0,0 +1,17 @@
+# Config file for the Cast Action Script tests.
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cloudcast.cast_action_script_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastActionScriptRunnerTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Comma-separated list of apps to test.
+# YT Music, YT, GPM, Spotify, TuneIn
+APP_ID_LIST=2DB7CC49,233637DE,F05C50C9,DF630089,12F05308
diff --git a/lab_system/master/config/tests/cloudcast/cloudcast_c4a.cfg b/lab_system/master/config/tests/cloudcast/cloudcast_c4a.cfg
new file mode 100644
index 0000000..5a6f337
--- /dev/null
+++ b/lab_system/master/config/tests/cloudcast/cloudcast_c4a.cfg
@@ -0,0 +1,17 @@
+# Config file for the Cast Action Script tests.
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cloudcast.cast_action_script_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastActionScriptRunnerTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Comma-separated list of apps to test.
+# YT Music, GPM, Pandora, Spotify, TuneIn
+APP_ID_LIST=2DB7CC49,F05C50C9,36061251,DF630089,12F05308
diff --git a/lab_system/master/config/tests/cloudcast/test_defs.json b/lab_system/master/config/tests/cloudcast/test_defs.json
new file mode 100644
index 0000000..475be1c
--- /dev/null
+++ b/lab_system/master/config/tests/cloudcast/test_defs.json
@@ -0,0 +1,47 @@
+[
+  {
+    "group_name": "cloudcast",
+    "slave_name": "cloudcast_video",
+    "test_name": "cloudcast",
+    "identifier": "cloudcast_eureka",
+    "device_type": "eureka",
+    "device_group": "eureka_debug",
+    "branch": ["master"]
+  },
+  {
+    "group_name": "cloudcast",
+    "slave_name": "cloudcast_video",
+    "test_name": "cloudcast",
+    "identifier": "cloudcast_lexx",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "branch": ["master"]
+  },
+  {
+    "group_name": "cloudcast",
+    "slave_name": "cloudcast_video",
+    "test_name": "cloudcast",
+    "identifier": "cloudcast_earth",
+    "device_type": "earth",
+    "device_group": "earth_eth",
+    "branch": ["master"]
+  },
+  {
+    "group_name": "cloudcast",
+    "slave_name": "cloudcast_audio",
+    "test_name": "cloudcast_c4a",
+    "identifier": "cloudcast_hendrix",
+    "device_type": "hendrix",
+    "device_group": "hendrix_eth",
+    "branch": ["master"]
+  },
+  {
+    "group_name": "cloudcast",
+    "slave_name": "cloudcast_audio",
+    "test_name": "cloudcast_c4a",
+    "identifier": "cloudcast_chirp",
+    "device_type": "chirp",
+    "device_group": "chirp",
+    "branch": ["master"]
+  }
+]
diff --git a/lab_system/master/config/tests/comms/comms_back_to_back_telstra_audio_call.cfg b/lab_system/master/config/tests/comms/comms_back_to_back_telstra_audio_call.cfg
new file mode 100644
index 0000000..0b7a877
--- /dev/null
+++ b/lab_system/master/config/tests/comms/comms_back_to_back_telstra_audio_call.cfg
@@ -0,0 +1,31 @@
+# Config file for multiple back to back telstra audio calls tests.
+# In this test case, we collect system performance metrics during the test.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.comms.carrier_audio_call_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CommsTelstraBackToBackAudioCallsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.comms.comms_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CommsTestSequence
+
+# Carrier testing provider
+CALL_PROVIDER=telstra
+
+# Carrier callee
+CALL_DESTINATION=+61393888068
+
+# Carrier caller
+CALL_CALLER=+17735550111
+
+# Max duration in seconds for each carrier call in the test
+CALL_DURATION_SECS=20
+
+# Number of the carrier calls being made in the test
+CALL_REPEAT_TIMES=30
diff --git a/lab_system/master/config/tests/comms/comms_long_duo_audio_call.cfg b/lab_system/master/config/tests/comms/comms_long_duo_audio_call.cfg
new file mode 100644
index 0000000..e57a4bb
--- /dev/null
+++ b/lab_system/master/config/tests/comms/comms_long_duo_audio_call.cfg
@@ -0,0 +1,25 @@
+# Config file for system perf duo video call tests.
+# In this test case, we collect system performance metrics for a duo video call.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.comms.duo_audio_call_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CommsDuoLongAudioCallTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.comms.comms_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CommsTestSequence
+
+# Duo testing code Bot ID
+CALL_PROVIDER=bot:duo-bot-router-speech-bot
+
+# Max duration in seconds for each duo call in the test
+CALL_DURATION_SECS=3300
+
+# Number of the Duo calls being made in the test
+CALL_REPEAT_TIMES=1
diff --git a/lab_system/master/config/tests/comms/comms_long_duo_video_call.cfg b/lab_system/master/config/tests/comms/comms_long_duo_video_call.cfg
new file mode 100644
index 0000000..97af7d0
--- /dev/null
+++ b/lab_system/master/config/tests/comms/comms_long_duo_video_call.cfg
@@ -0,0 +1,26 @@
+# Config file for system perf duo video call tests.
+# In this test case, we collect system performance metrics for a duo video call.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.comms.duo_video_call_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CommsDuoLongVideoCallTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.comms.comms_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CommsTestSequence
+
+# Duo testing code Bot ID
+CALL_PROVIDER=bot:duo-bot-router-speech-bot
+
+# Max duration in seconds for each duo call in the test
+CALL_DURATION_SECS=3300
+
+# Number of the Duo calls being made in the test
+CALL_REPEAT_TIMES=1
+
diff --git a/lab_system/master/config/tests/comms/comms_long_telstra_audio_call.cfg b/lab_system/master/config/tests/comms/comms_long_telstra_audio_call.cfg
new file mode 100644
index 0000000..7b60390
--- /dev/null
+++ b/lab_system/master/config/tests/comms/comms_long_telstra_audio_call.cfg
@@ -0,0 +1,32 @@
+# Config file for long telstra audio call tests.
+# In this test case, we collect system performance metrics for a telstra audio call.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.comms.carrier_audio_call_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CommsTelstraLongAudioCallTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.comms.comms_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CommsTestSequence
+
+# Carrier testing provider
+CALL_PROVIDER=telstra
+
+# Carrier testing provider
+CALL_DESTINATION=+61267714906
+
+# Carrier testing provider
+CALL_CALLER=+17735550111
+
+# Max duration in seconds for each carrier call in the test
+CALL_DURATION_SECS=1800
+
+# Number of the carrier calls being made in the test
+CALL_REPEAT_TIMES=1
+
diff --git a/lab_system/master/config/tests/comms/comms_short_duo_audio_call.cfg b/lab_system/master/config/tests/comms/comms_short_duo_audio_call.cfg
new file mode 100644
index 0000000..906618e
--- /dev/null
+++ b/lab_system/master/config/tests/comms/comms_short_duo_audio_call.cfg
@@ -0,0 +1,25 @@
+# Config file for short duo audio call tests.
+# In this test case, we collect system performance metrics for a duo video call.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.comms.duo_audio_call_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CommsDuoShortAudioCallTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.comms.comms_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CommsTestSequence
+
+# Duo testing code Bot ID
+CALL_PROVIDER=bot:duo-bot-router-speech-bot
+
+# Max duration in seconds for each duo call in the test
+CALL_DURATION_SECS=300
+
+# Number of the Duo calls being made in the test
+CALL_REPEAT_TIMES=1
diff --git a/lab_system/master/config/tests/comms/comms_short_duo_video_call.cfg b/lab_system/master/config/tests/comms/comms_short_duo_video_call.cfg
new file mode 100644
index 0000000..79f8c53
--- /dev/null
+++ b/lab_system/master/config/tests/comms/comms_short_duo_video_call.cfg
@@ -0,0 +1,26 @@
+# Config file for short duo video call tests.
+# In this test case, we collect system performance metrics for a duo video call.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.comms.duo_video_call_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CommsDuoShortVideoCallTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.comms.comms_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CommsTestSequence
+
+# Duo testing code Bot ID
+CALL_PROVIDER=bot:duo-bot-router-speech-bot
+
+# Max duration in seconds for each duo call in the test
+CALL_DURATION_SECS=300
+
+# Number of the Duo calls being made in the test
+CALL_REPEAT_TIMES=1
+
diff --git a/lab_system/master/config/tests/comms/comms_short_telstra_audio_call.cfg b/lab_system/master/config/tests/comms/comms_short_telstra_audio_call.cfg
new file mode 100644
index 0000000..d6b54de
--- /dev/null
+++ b/lab_system/master/config/tests/comms/comms_short_telstra_audio_call.cfg
@@ -0,0 +1,32 @@
+# Config file for short telstra audio call tests.
+# In this test case, we collect system performance metrics for a telstra audio call.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.comms.carrier_audio_call_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CommsTelstraShortAudioCallTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.comms.comms_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CommsTestSequence
+
+# Carrier testing provider
+CALL_PROVIDER=telstra
+
+# Carrier testing provider
+CALL_DESTINATION=+61267714906
+
+# Carrier testing provider
+CALL_CALLER=+17735550111
+
+# Max duration in seconds for each carrier call in the test
+CALL_DURATION_SECS=300
+
+# Number of the carrier calls being made in the test
+CALL_REPEAT_TIMES=1
+
diff --git a/lab_system/master/config/tests/cpu_load/cpu_load.cfg b/lab_system/master/config/tests/cpu_load/cpu_load.cfg
new file mode 100644
index 0000000..dd227aa
--- /dev/null
+++ b/lab_system/master/config/tests/cpu_load/cpu_load.cfg
@@ -0,0 +1,13 @@
+# Configuration values for the cpu_load benchmarks test.
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cpu_load.cpu_load_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CpuLoadTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.cpu_load.cpu_load_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CpuLoadTestSequence
diff --git a/lab_system/master/config/tests/cpu_load/test_defs.json b/lab_system/master/config/tests/cpu_load/test_defs.json
new file mode 100644
index 0000000..5420743
--- /dev/null
+++ b/lab_system/master/config/tests/cpu_load/test_defs.json
@@ -0,0 +1,57 @@
+[
+  {
+    "group_name": "cpu_load",
+    "slave_name": "cpu_load",
+    "test_name": "cpu_load",
+    "identifier": "cpu_load",
+    "device_type": "eureka",
+    "device_group": "eureka_debug",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "cpu_load",
+    "slave_name": "cpu_load_v2",
+    "test_name": "cpu_load",
+    "identifier": "cpu_load_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "cpu_load",
+    "slave_name": "cpu_load_v3",
+    "test_name": "cpu_load",
+    "identifier": "cpu_load_v3",
+    "device_type": "earth",
+    "device_group": "earth_eth",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "cpu_load",
+    "slave_name": "cpu_load_audio",
+    "test_name": "cpu_load",
+    "identifier": "cpu_load_hendrix",
+    "device_type": "hendrix",
+    "device_group": "hendrix_eth",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "cpu_load",
+    "slave_name": "cpu_load",
+    "test_name": "cpu_load",
+    "identifier": "cpu_load_v4",
+    "device_type": "luther_b2",
+    "device_group": "luther_eth",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "build_flavor": "user"
+  }
+]
diff --git a/lab_system/master/config/tests/crash_loop/crash_checks.cfg b/lab_system/master/config/tests/crash_loop/crash_checks.cfg
new file mode 100644
index 0000000..4ac9ea6
--- /dev/null
+++ b/lab_system/master/config/tests/crash_loop/crash_checks.cfg
@@ -0,0 +1,17 @@
+# Config file for the Crash/Boot loop tests
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ota_installable.crash_checks_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CrashChecksTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.ota_installable.ota_installable_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OtaInstallableTestSequence
+
+
diff --git a/lab_system/master/config/tests/crash_loop/crash_loop.cfg b/lab_system/master/config/tests/crash_loop/crash_loop.cfg
new file mode 100644
index 0000000..cf4b1b7
--- /dev/null
+++ b/lab_system/master/config/tests/crash_loop/crash_loop.cfg
@@ -0,0 +1,17 @@
+# Config file for the Crash/Boot loop tests
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ota_installable.crash_loop_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CrashLoopTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.ota_installable.ota_installable_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OtaInstallableTestSequence
+
+
diff --git a/lab_system/master/config/tests/crash_loop/fuchsia_crash_loop.cfg b/lab_system/master/config/tests/crash_loop/fuchsia_crash_loop.cfg
new file mode 100644
index 0000000..5492b81
--- /dev/null
+++ b/lab_system/master/config/tests/crash_loop/fuchsia_crash_loop.cfg
@@ -0,0 +1,17 @@
+# Config file for the Crash/Boot loop tests on Fuchsia devices
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ota_installable.fuchsia_crash_loop_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FuchsiaCrashLoopTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.ota_installable.fuchsia_ota_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaOtaInstallableTestSequence
+
+
diff --git a/lab_system/master/config/tests/cycle_bitrates/cycle_bitrates.cfg b/lab_system/master/config/tests/cycle_bitrates/cycle_bitrates.cfg
new file mode 100644
index 0000000..b077223
--- /dev/null
+++ b/lab_system/master/config/tests/cycle_bitrates/cycle_bitrates.cfg
@@ -0,0 +1,17 @@
+# Config file for JavaScript video tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cycle_bitrates.cycle_bitrates_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CycleBitRatesTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+
diff --git a/lab_system/master/config/tests/cycle_bitrates/fuchsia_cycle_bitrates.cfg b/lab_system/master/config/tests/cycle_bitrates/fuchsia_cycle_bitrates.cfg
new file mode 100644
index 0000000..b4d90b1
--- /dev/null
+++ b/lab_system/master/config/tests/cycle_bitrates/fuchsia_cycle_bitrates.cfg
@@ -0,0 +1,17 @@
+# Config file for JavaScript video tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cycle_bitrates.cycle_bitrates_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CycleBitRatesTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.fuchsia_javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaJavascriptTestSequence
+
+
diff --git a/lab_system/master/config/tests/cycle_bitrates/test_defs.json b/lab_system/master/config/tests/cycle_bitrates/test_defs.json
new file mode 100644
index 0000000..0b1c865
--- /dev/null
+++ b/lab_system/master/config/tests/cycle_bitrates/test_defs.json
@@ -0,0 +1,11 @@
+[
+  {
+    "group_name": "cycle_bitrates",
+    "slave_name": "cycle_bitrates",
+    "test_name": "cycle_bitrates",
+    "identifier": "cycle_bitrates_cast_tv",
+    "device_type": "dragonfly",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_dragonfly_master", "cast_tv_dragonfly_release", "cast_tv_dragonfly_next_release"]
+  }
+]
diff --git a/lab_system/master/config/tests/dcs/atv_dcs.cfg b/lab_system/master/config/tests/dcs/atv_dcs.cfg
new file mode 100644
index 0000000..addeacc
--- /dev/null
+++ b/lab_system/master/config/tests/dcs/atv_dcs.cfg
@@ -0,0 +1,15 @@
+# Configuration values for the dcs test.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.dcs.atv_dcs_e2e_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AtvDcsE2eTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.android_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidTestSequence
diff --git a/lab_system/master/config/tests/dcs/dcs.cfg b/lab_system/master/config/tests/dcs/dcs.cfg
new file mode 100644
index 0000000..4748ac6
--- /dev/null
+++ b/lab_system/master/config/tests/dcs/dcs.cfg
@@ -0,0 +1,17 @@
+# Configuration values for the dcs test.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.dcs.dcs_e2e_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=DcsE2eTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+
diff --git a/lab_system/master/config/tests/dcs/fuchsia_dcs.cfg b/lab_system/master/config/tests/dcs/fuchsia_dcs.cfg
new file mode 100644
index 0000000..85b90f2
--- /dev/null
+++ b/lab_system/master/config/tests/dcs/fuchsia_dcs.cfg
@@ -0,0 +1,17 @@
+# Configuration values for the Fuchsia dcs test.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.dcs.fuchsia_dcs_e2e_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FuchsiaDcsE2eTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.fuchsia_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaTestSequence
+
+
diff --git a/lab_system/master/config/tests/dcs/test_defs.json b/lab_system/master/config/tests/dcs/test_defs.json
new file mode 100644
index 0000000..65cd508
--- /dev/null
+++ b/lab_system/master/config/tests/dcs/test_defs.json
@@ -0,0 +1,47 @@
+[
+  {
+    "group_name": "dcs",
+    "slave_name": "eurtest_dcs",
+    "test_name": "dcs",
+    "identifier": "dcs_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "dcs",
+    "slave_name": "eurtest_dcs",
+    "test_name": "dcs",
+    "identifier": "dcs_v3",
+    "device_type": "earth",
+    "device_group": "earth_eth",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "dcs",
+    "slave_name": "eurtest_dcs",
+    "test_name": "dcs",
+    "identifier": "dcs_hendrix",
+    "device_type": "hendrix",
+    "device_group": "hendrix_eth",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "dcs",
+    "slave_name": "eurtest_dcs",
+    "test_name": "dcs",
+    "identifier": "dcs_v4",
+    "device_type": "luther_b2",
+    "device_group": "luther_eth",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "dcs",
+    "slave_name": "eurtest_dcs",
+    "test_name": "dcs",
+    "identifier": "dcs_chirp",
+    "device_type": "chirp",
+    "device_group": "chirp",
+    "branch": ["master", "release"]
+  }
+]
diff --git a/lab_system/master/config/tests/display_assistant_screen/display_assistant_screen.cfg b/lab_system/master/config/tests/display_assistant_screen/display_assistant_screen.cfg
new file mode 100644
index 0000000..50603f6
--- /dev/null
+++ b/lab_system/master/config/tests/display_assistant_screen/display_assistant_screen.cfg
@@ -0,0 +1,10 @@
+# Config file for display assistant screen tests.
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.display_assistant_screen.display_assistant_screen_test_case
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=DisplayAssistantScreenTestCase
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
diff --git a/lab_system/master/config/tests/drm/drm.cfg b/lab_system/master/config/tests/drm/drm.cfg
new file mode 100644
index 0000000..6c6cef1
--- /dev/null
+++ b/lab_system/master/config/tests/drm/drm.cfg
@@ -0,0 +1,17 @@
+# Config file for JavaScript video tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.drm.drm_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=DrmPlayreadySmoothTestCase,DrmPlayreadyTestCase,DrmTestCase,DrmUnprefixedTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+
diff --git a/lab_system/master/config/tests/drm/test_defs.json b/lab_system/master/config/tests/drm/test_defs.json
new file mode 100644
index 0000000..fbb425e
--- /dev/null
+++ b/lab_system/master/config/tests/drm/test_defs.json
@@ -0,0 +1,11 @@
+[
+  {
+    "group_name": "drm",
+    "slave_name": "drm",
+    "test_name": "drm",
+    "identifier": "drm_cast_tv",
+    "device_type": "dragonfly",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_dragonfly_master", "cast_tv_dragonfly_release", "cast_tv_dragonfly_next_release"]
+  }
+]
diff --git a/lab_system/master/config/tests/e2e_latency/e2e_latency.cfg b/lab_system/master/config/tests/e2e_latency/e2e_latency.cfg
new file mode 100644
index 0000000..c9f9ed1
--- /dev/null
+++ b/lab_system/master/config/tests/e2e_latency/e2e_latency.cfg
@@ -0,0 +1,26 @@
+# Config file for Chirp End 2 End latency tests.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.e2e_latency.e2e_query_latency_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=E2eQueryLatencyTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.e2e_latency.e2e_query_latency_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=E2eQueryLatencyTestSequence
+
+# Never backtrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
+# Use Tunnel where applicable
+
diff --git a/lab_system/master/config/tests/e2e_latency/e2e_latency_reduced.cfg b/lab_system/master/config/tests/e2e_latency/e2e_latency_reduced.cfg
new file mode 100644
index 0000000..c834bd3
--- /dev/null
+++ b/lab_system/master/config/tests/e2e_latency/e2e_latency_reduced.cfg
@@ -0,0 +1,26 @@
+# Config file for Chirp End 2 End latency tests.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.e2e_latency.e2e_query_latency_reduced_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=E2eQueryLatencyReducedTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.e2e_latency.e2e_query_latency_reduced_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=E2eQueryLatencyReducedTestSequence
+
+# Never backtrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
+# Use Tunnel where applicable
+
diff --git a/lab_system/master/config/tests/e2e_latency/imx7d_e2e_latency.cfg b/lab_system/master/config/tests/e2e_latency/imx7d_e2e_latency.cfg
new file mode 100644
index 0000000..14721c0
--- /dev/null
+++ b/lab_system/master/config/tests/e2e_latency/imx7d_e2e_latency.cfg
@@ -0,0 +1,24 @@
+# Config file for Chirp End 2 End latency tests.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.e2e_latency.e2e_query_latency_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=E2eQueryLatencyTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.e2e_latency.e2e_query_latency_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=E2eQueryLatencyTestSequence
+
+# Never backtrack so service-related failures have a
+# chronological correspondence to build numbers.
+BUILD_SELECTION_POLICY=always_use_latest
+
diff --git a/lab_system/master/config/tests/e2e_latency/test_defs.json b/lab_system/master/config/tests/e2e_latency/test_defs.json
new file mode 100644
index 0000000..16b5d95
--- /dev/null
+++ b/lab_system/master/config/tests/e2e_latency/test_defs.json
@@ -0,0 +1,156 @@
+[
+  {
+    "group_name": "e2e_latency",
+    "slave_name": "chirp_e2e_enclosures",
+    "test_name": "e2e_latency_reduced",
+    "identifier": "chirp_e2e_latency_reduced_master",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_e2e_enclosure",
+    "branch": "master",
+    "scheduler": "30 * * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "e2e_latency",
+    "slave_name": "chirp_e2e_enclosures",
+    "test_name": "e2e_latency_reduced",
+    "identifier": "chirp_e2e_latency_reduced_next_release",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_e2e_enclosure",
+    "branch": "next_release",
+    "scheduler": "0 * * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "e2e_latency",
+    "slave_name": "chirp_e2e_enclosures",
+    "test_name": "e2e_latency_reduced",
+    "identifier": "chirp_e2e_latency_reduced_release",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_e2e_enclosure",
+    "branch": "release",
+    "scheduler": "30 0,6,12,18 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "e2e_latency",
+    "slave_name": "joplin_e2e_enclosures",
+    "test_name": "e2e_latency_reduced",
+    "identifier": "joplin_e2e_latency_reduced_master",
+    "device_type": "joplin",
+    "device_group": "joplin_e2e_enclosure",
+    "branch": "master",
+    "scheduler": "30 * * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "e2e_latency",
+    "slave_name": "joplin_e2e_enclosures",
+    "test_name": "e2e_latency_reduced",
+    "identifier": "joplin_e2e_latency_reduced_next_release",
+    "device_type": "joplin",
+    "device_group": "joplin_e2e_enclosure",
+    "branch": "1_28a",
+    "scheduler": "0 * * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "e2e_latency",
+    "slave_name": "biggie_e2e_enclosures",
+    "test_name": "e2e_latency_reduced",
+    "identifier": "biggie_e2e_latency_reduced_master",
+    "device_type": "biggie",
+    "device_group": "biggie_e2e_enclosure",
+    "branch": "master",
+    "scheduler": "30 * * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "e2e_latency",
+    "slave_name": "biggie_e2e_enclosures",
+    "test_name": "e2e_latency_reduced",
+    "identifier": "biggie_e2e_latency_reduced_next_release",
+    "device_type": "biggie",
+    "device_group": "biggie_e2e_enclosure",
+    "branch": "next_release",
+    "scheduler": "0 * * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/tests/eraser_for_hotword/eraser.cfg b/lab_system/master/config/tests/eraser_for_hotword/eraser.cfg
new file mode 100644
index 0000000..42b5d20
--- /dev/null
+++ b/lab_system/master/config/tests/eraser_for_hotword/eraser.cfg
@@ -0,0 +1,32 @@
+# Config file for Eraser tests on Biggie.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Device options
+CHIRP_VOL_MIN=0
+CHIRP_VOL_MAX=11
+CHIRP_VOL_STEP=1
+RPI_VOL_MIN=1
+RPI_VOL_MAX=100
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=EraserForHotwordTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.eraser.eraser_for_hotword_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=EraserForHotwordTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.eraser.eraser_for_hotword_test_sequence
diff --git a/lab_system/master/config/tests/eraser_for_hotword/eraser_alignment.cfg b/lab_system/master/config/tests/eraser_for_hotword/eraser_alignment.cfg
new file mode 100644
index 0000000..274a56a
--- /dev/null
+++ b/lab_system/master/config/tests/eraser_for_hotword/eraser_alignment.cfg
@@ -0,0 +1,24 @@
+# Config file for Eraser alignment tests.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+ENABLE_CAST_WITHOUT_SIGN_IN=True
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=EraserAlignmentTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.eraser.eraser_alignment_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=EraserAlignmentTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.eraser.eraser_alignment_test_sequence
\ No newline at end of file
diff --git a/lab_system/master/config/tests/eraser_for_hotword/eraser_alignment_fuchsia.cfg b/lab_system/master/config/tests/eraser_for_hotword/eraser_alignment_fuchsia.cfg
new file mode 100644
index 0000000..03bba24
--- /dev/null
+++ b/lab_system/master/config/tests/eraser_for_hotword/eraser_alignment_fuchsia.cfg
@@ -0,0 +1,19 @@
+# Config file for Eraser alignment tests.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=EraserAlignmentTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.eraser.eraser_alignment_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaEraserAlignmentTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.eraser.fuchsia_eraser_alignment_test_sequence
\ No newline at end of file
diff --git a/lab_system/master/config/tests/eraser_for_hotword/eraser_fuchsia.cfg b/lab_system/master/config/tests/eraser_for_hotword/eraser_fuchsia.cfg
new file mode 100644
index 0000000..c692c99
--- /dev/null
+++ b/lab_system/master/config/tests/eraser_for_hotword/eraser_fuchsia.cfg
@@ -0,0 +1,29 @@
+# Config file for Eraser tests on Fuchsia.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Device options
+CHIRP_VOL_MIN=0
+CHIRP_VOL_MAX=11
+CHIRP_VOL_STEP=1
+RPI_VOL_MIN=1
+RPI_VOL_MAX=100
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=EraserForHotwordTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.eraser.eraser_for_hotword_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaEraserForHotwordTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.eraser.fuchsia_eraser_for_hotword_test_sequence
diff --git a/lab_system/master/config/tests/eraser_for_hotword/eraser_realignment.cfg b/lab_system/master/config/tests/eraser_for_hotword/eraser_realignment.cfg
new file mode 100644
index 0000000..ac1965c
--- /dev/null
+++ b/lab_system/master/config/tests/eraser_for_hotword/eraser_realignment.cfg
@@ -0,0 +1,22 @@
+# Config file for Eraser Realignment test.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.eraser.eraser_alignment_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=EraserRealignmentTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.eraser.eraser_alignment_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=EraserRealignmentTestSequence
diff --git a/lab_system/master/config/tests/eraser_for_hotword/eraser_reset_realignment.cfg b/lab_system/master/config/tests/eraser_for_hotword/eraser_reset_realignment.cfg
new file mode 100644
index 0000000..8b8d719
--- /dev/null
+++ b/lab_system/master/config/tests/eraser_for_hotword/eraser_reset_realignment.cfg
@@ -0,0 +1,22 @@
+# Config file for Eraser Realignment test.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.eraser.eraser_alignment_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=EraserResetRealignmentTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.eraser.eraser_alignment_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=EraserResetRealignmentTestSequence
diff --git a/lab_system/master/config/tests/eraser_for_hotword/eraser_reset_realignment_fuchsia.cfg b/lab_system/master/config/tests/eraser_for_hotword/eraser_reset_realignment_fuchsia.cfg
new file mode 100644
index 0000000..7769e92
--- /dev/null
+++ b/lab_system/master/config/tests/eraser_for_hotword/eraser_reset_realignment_fuchsia.cfg
@@ -0,0 +1,19 @@
+# Config file for Eraser Realignment test.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.eraser.eraser_alignment_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=EraserResetRealignmentTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.eraser.fuchsia_eraser_alignment_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaEraserResetRealignmentTestSequence
diff --git a/lab_system/master/config/tests/eraser_for_hotword/eraser_suppression.cfg b/lab_system/master/config/tests/eraser_for_hotword/eraser_suppression.cfg
new file mode 100644
index 0000000..00c91fb
--- /dev/null
+++ b/lab_system/master/config/tests/eraser_for_hotword/eraser_suppression.cfg
@@ -0,0 +1,22 @@
+# Config file for Eraser suppression tests.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.eraser.eraser_suppression_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=EraserAlignmentSuppressionTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.eraser.eraser_suppression_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=EraserAlignmentSuppressionTestSequence
+
+
+ENABLE_CAST_WITHOUT_SIGN_IN=True
diff --git a/lab_system/master/config/tests/eraser_for_hotword/test_defs.json b/lab_system/master/config/tests/eraser_for_hotword/test_defs.json
new file mode 100644
index 0000000..80dfde5
--- /dev/null
+++ b/lab_system/master/config/tests/eraser_for_hotword/test_defs.json
@@ -0,0 +1,156 @@
+[
+  {
+    "group_name": "eraser_for_hotword",
+    "slave_name": "chirp_eraser_linda_vista",
+    "test_name": "eraser",
+    "identifier": "chirp_eraser_linda_vista_master",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_eraser_linda_vista",
+    "branch": "master",
+    "scheduler": "0 0,3,6,9,12,15,18,21 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "eraser_for_hotword",
+    "slave_name": "chirp_eraser_linda_vista",
+    "test_name": "eraser",
+    "identifier": "chirp_eraser_linda_vista_next_release",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_eraser_linda_vista",
+    "branch": "next_release",
+    "scheduler": "0 1,4,7,10,13,16,19,22 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "eraser_for_hotword",
+    "slave_name": "chirp_eraser_linda_vista",
+    "test_name": "eraser",
+    "identifier": "chirp_eraser_linda_vista_release",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_eraser_linda_vista",
+    "branch": "release",
+    "scheduler": "0 2,5,8,11,14,17,20,23 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "eraser_for_hotword",
+    "slave_name": "joplin_eraser_linda_vista",
+    "test_name": "eraser",
+    "identifier": "joplin_eraser_linda_vista_master",
+    "device_type": "joplin",
+    "device_group": "joplin_eraser_linda_vista",
+    "branch": "master",
+    "scheduler": "0 0,2,4,6,8,10,12,14,16,18,20,22 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "eraser_for_hotword",
+    "slave_name": "joplin_eraser_linda_vista",
+    "test_name": "eraser",
+    "identifier": "joplin_eraser_linda_vista_next_release",
+    "device_type": "joplin",
+    "device_group": "joplin_eraser_linda_vista",
+    "branch": "1_28a",
+    "scheduler": "0 1,3,5,7,9,11,13,15,17,19,21,23 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "eraser_for_hotword",
+    "slave_name": "biggie_eraser_linda_vista",
+    "test_name": "eraser",
+    "identifier": "biggie_eraser_linda_vista_master",
+    "device_type": "biggie_b3",
+    "device_group": "biggie_eraser_linda_vista",
+    "branch": "master",
+    "scheduler": "0 0,2,4,18,20,22 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "eraser_for_hotword",
+    "slave_name": "biggie_eraser_linda_vista",
+    "test_name": "eraser",
+    "identifier": "biggie_eraser_linda_vista_eraser_next_release",
+    "device_type": "biggie_b3",
+    "device_group": "biggie_eraser_linda_vista",
+    "branch": "next_release",
+    "scheduler": "0 1,3,19,21,23 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/tests/factory/fct_biggie.cfg b/lab_system/master/config/tests/factory/fct_biggie.cfg
new file mode 100644
index 0000000..d0b7774
--- /dev/null
+++ b/lab_system/master/config/tests/factory/fct_biggie.cfg
@@ -0,0 +1,29 @@
+# Config file for factory tests using biggie.
+#
+# PROPERTIES
+USER_NAME=root
+# Test work directory on remote slave
+REMOTE_FCT_TEST_DIR=/root/biggie_fct
+REMOTE_FCT_LOAD_SCRIPT=/root/biggie_fct/aml-usb-boot/linux/aml-usb-fct-boot.sh
+FCT_BUILD_INSTALLER_PATH=/usr/local/google/repositories/HEAD/test/ota/fct_update.py
+# Scripts path avialable on the target device.
+FCT_SCRIPT_NAME=/home/flex/bin/fct.sh
+FCT_USE_ADB_SHELL=True
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.factory.fct_hw_test_cases
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FctHwCheckTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.factory.fct_test_sequence
+
+# # Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FctTestSequence
+
+# Devices group
+DEVICES_GROUP=fct_test_box
+
+# Use Tunnel where applicable
+
diff --git a/lab_system/master/config/tests/factory/test_defs.json b/lab_system/master/config/tests/factory/test_defs.json
new file mode 100644
index 0000000..288c0df
--- /dev/null
+++ b/lab_system/master/config/tests/factory/test_defs.json
@@ -0,0 +1,13 @@
+[
+    {
+        "branch": ["master", "1_28_factory"],
+        "build_flavor": "eng",
+        "device_group": "fct_test_box",
+        "device_type": "biggie",
+        "identifier": "fct_biggie",
+        "slave_name": "factory",
+        "group_name": "factory",
+        "test_name": "fct_biggie",
+        "step_factories_recipe": "step_factories_recipes/fct_test_recipe.json"
+    }
+]
diff --git a/lab_system/master/config/tests/fakeoculus_reconnect_proxy/fakeoculus_reconnect_proxy.cfg b/lab_system/master/config/tests/fakeoculus_reconnect_proxy/fakeoculus_reconnect_proxy.cfg
new file mode 100644
index 0000000..947fff5
--- /dev/null
+++ b/lab_system/master/config/tests/fakeoculus_reconnect_proxy/fakeoculus_reconnect_proxy.cfg
@@ -0,0 +1,22 @@
+# Configuration values for FakeOculus Reconnect Proxy test.
+#
+# PROPERTIES
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FakeOculusReconnectTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.fakeoculus_reconnect_proxy.fakeoculus_reconnect_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FakeoculusReconnectTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.fakeoculus_reconnect_proxy.fakeoculus_reconnect_test_sequence
+
+# If running in the lab, set to True as it will use the LAB default
+# IP instead of using shortleash gateway IP
+LAB_HOST_IP=true
+
+# Specifies which folder to consider for JSON test flow files.
+# This is under the fakeoculus_test_scenarios folder.
+TEST_JSON_LOCATION=functional_tests
\ No newline at end of file
diff --git a/lab_system/master/config/tests/fakeoculus_reconnect_proxy/fakeoculus_stress_tests.cfg b/lab_system/master/config/tests/fakeoculus_reconnect_proxy/fakeoculus_stress_tests.cfg
new file mode 100644
index 0000000..e3caff5
--- /dev/null
+++ b/lab_system/master/config/tests/fakeoculus_reconnect_proxy/fakeoculus_stress_tests.cfg
@@ -0,0 +1,22 @@
+# Configuration values for FakeOculus Reconnect Proxy test.
+#
+# PROPERTIES
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FakeOculusReconnectStressTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.fakeoculus_reconnect_proxy.fakeoculus_reconnect_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FakeoculusReconnectTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.fakeoculus_reconnect_proxy.fakeoculus_reconnect_test_sequence
+
+# If running in the lab, set to True as it will use the LAB default
+# IP instead of using shortleash gateway IP
+LAB_HOST_IP=true
+
+# Specifies which folder to consider for JSON test flow files.
+# This is under the fakeoculus_test_scenarios folder.
+TEST_JSON_LOCATION=stream_stress_tests
\ No newline at end of file
diff --git a/lab_system/master/config/tests/fdr/fdr.cfg b/lab_system/master/config/tests/fdr/fdr.cfg
new file mode 100644
index 0000000..c9a5386
--- /dev/null
+++ b/lab_system/master/config/tests/fdr/fdr.cfg
@@ -0,0 +1,18 @@
+# Config file for FDR tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.fdr.fdr_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FdrTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
+
+# Use Tunnel where applicable
+
diff --git a/lab_system/master/config/tests/fuchsia_audio_formats/fuchsia_audio_formats.cfg b/lab_system/master/config/tests/fuchsia_audio_formats/fuchsia_audio_formats.cfg
new file mode 100644
index 0000000..6297ef0
--- /dev/null
+++ b/lab_system/master/config/tests/fuchsia_audio_formats/fuchsia_audio_formats.cfg
@@ -0,0 +1,14 @@
+# Config file for JavaScript audio format tests on Fuchsia devices.
+#
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.audio_formats.audio_formats_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SupportedAudioFormatsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.fuchsia_javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaJavascriptTestSequence
+
diff --git a/lab_system/master/config/tests/fuchsia_drm/fuchsia_drm.cfg b/lab_system/master/config/tests/fuchsia_drm/fuchsia_drm.cfg
new file mode 100644
index 0000000..ab47016
--- /dev/null
+++ b/lab_system/master/config/tests/fuchsia_drm/fuchsia_drm.cfg
@@ -0,0 +1,16 @@
+# Config file for JavaScript tests on Fuchsia devices.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.drm.fuchsia_drm_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FuchsiaDrmTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.fuchsia_javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaJavascriptTestSequence
+
diff --git a/lab_system/master/config/tests/fuchsia_gunit/fuchsia_gunit.cfg b/lab_system/master/config/tests/fuchsia_gunit/fuchsia_gunit.cfg
new file mode 100644
index 0000000..dbdcf73
--- /dev/null
+++ b/lab_system/master/config/tests/fuchsia_gunit/fuchsia_gunit.cfg
@@ -0,0 +1,14 @@
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.fuchsia_gunit.fuchsia_gunit_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FuchsiagUnitTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.fuchsia_gunit.fuchsia_gunit_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiagUnitTestSequence
+ITERATION_COUNT=10
diff --git a/lab_system/master/config/tests/fuchsia_migration/fuchsia_migration.cfg b/lab_system/master/config/tests/fuchsia_migration/fuchsia_migration.cfg
new file mode 100644
index 0000000..116131d
--- /dev/null
+++ b/lab_system/master/config/tests/fuchsia_migration/fuchsia_migration.cfg
@@ -0,0 +1,22 @@
+# Config file for OTA update test suite on master branch.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.fuchsia_migration.fuchsia_migration_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FuchsiaMigrationTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.fuchsia_migration.fuchsia_migration_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaMigrationTestSequence
+
+# OTA update master version. fetcher will use the value to download latest OTA.
+BRANCH_TO_TEST=master
+
+BUILD_SIGNATURE=dev_signed
+
+USE_WIFI=True
diff --git a/lab_system/master/config/tests/gating/dcs.cfg b/lab_system/master/config/tests/gating/dcs.cfg
new file mode 100644
index 0000000..4748ac6
--- /dev/null
+++ b/lab_system/master/config/tests/gating/dcs.cfg
@@ -0,0 +1,17 @@
+# Configuration values for the dcs test.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.dcs.dcs_e2e_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=DcsE2eTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+
diff --git a/lab_system/master/config/tests/gating/fdr.cfg b/lab_system/master/config/tests/gating/fdr.cfg
new file mode 100644
index 0000000..c9a5386
--- /dev/null
+++ b/lab_system/master/config/tests/gating/fdr.cfg
@@ -0,0 +1,18 @@
+# Config file for FDR tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.fdr.fdr_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FdrTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
+
+# Use Tunnel where applicable
+
diff --git a/lab_system/master/config/tests/gating/hs3_assistant_test.cfg b/lab_system/master/config/tests/gating/hs3_assistant_test.cfg
new file mode 100644
index 0000000..5a09d15
--- /dev/null
+++ b/lab_system/master/config/tests/gating/hs3_assistant_test.cfg
@@ -0,0 +1,17 @@
+# Config file for hermetic S3 smoke tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hs3_assistant_test.hs3_assistant_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=Hs3AssistantTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hs3_assistant_test.hs3_assistant_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=Hs3AssistantTestSequence
+
+BUILD_SELECTION_POLICY=backtrack_then_latest
diff --git a/lab_system/master/config/tests/gating/ota_update.cfg b/lab_system/master/config/tests/gating/ota_update.cfg
new file mode 100644
index 0000000..b9d8d3a
--- /dev/null
+++ b/lab_system/master/config/tests/gating/ota_update.cfg
@@ -0,0 +1,23 @@
+# Config file for OTA update test suite on master branch.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ota_update.ota_update_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OtaUpdateTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
+
+# Get second to latest build
+BUILD_SELECTION_POLICY=second_to_latest
+
+# OTA update master version. fetcher will use the value to download latest OTA.
+BRANCH_TO_TEST=master
+
+
diff --git a/lab_system/master/config/tests/gating/receiver.cfg b/lab_system/master/config/tests/gating/receiver.cfg
new file mode 100644
index 0000000..df33a7c
--- /dev/null
+++ b/lab_system/master/config/tests/gating/receiver.cfg
@@ -0,0 +1,41 @@
+# Config file for the Cast receiver tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.compliance_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverComplianceTestCase,CastReceiverMultipleSendersTestCase,CastReceiverVolumeControlTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+CHROME_MIRRORING_APP_ID=0F5096E8
+
+# Test specific configuration values:
+MP4_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/mp4/bbb_1080p_30fps_x264_high_41_mp3.mp4
+MP4_MEDIA_MIME_TYPE=video/mp4
+DASH_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/dash/sample-dash-list.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+HLS_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/hls/bbb/bbb_hls.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+SMOOTH_STREAMING_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/smooth/BBB_H264_AACL/big_buck_bunny_1080p_stereo.ism/manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+MEDIA_LOAD_TIMEOUT_SECS=30
+
+# This config is used to determine if cast operations should be allowed even if
+# device is not signed-in using a gaia account.
+ENABLE_CAST_WITHOUT_SIGN_IN=True
+
+
diff --git a/lab_system/master/config/tests/gating/receiver_c4a.cfg b/lab_system/master/config/tests/gating/receiver_c4a.cfg
new file mode 100644
index 0000000..3450125
--- /dev/null
+++ b/lab_system/master/config/tests/gating/receiver_c4a.cfg
@@ -0,0 +1,46 @@
+# Config file for the Cast receiver tests on audio devices
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.compliance_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverComplianceTestCase,CastReceiverMultipleSendersTestCase,CastReceiverVolumeControlTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+CHROME_MIRRORING_APP_ID=85CDB22F
+
+# Test specific configuration values:
+MP4_MEDIA_URL=https://storage.googleapis.com/cast_media/format_tests/audio/stereo_guitar.mp3
+MP4_MEDIA_MIME_TYPE=video/mp4
+
+# Temporary fix
+DASH_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/dash/sample-dash-list.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+HLS_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/hls/bbb/bbb_hls.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+SMOOTH_STREAMING_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/smooth/BBB_H264_AACL/big_buck_bunny_1080p_stereo.ism/manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+
+MEDIA_LOAD_TIMEOUT_SECS=30
+
+# Flag to skip tests that require a display and can't be run in C4A devices.
+NO_DISPLAY=1
+
+# This config is used to determine if cast operations should be allowed even if
+# device is not signed-in using a gaia account.
+ENABLE_CAST_WITHOUT_SIGN_IN=True
+
diff --git a/lab_system/master/config/tests/gating/setup.cfg b/lab_system/master/config/tests/gating/setup.cfg
new file mode 100644
index 0000000..fb80ae7
--- /dev/null
+++ b/lab_system/master/config/tests/gating/setup.cfg
@@ -0,0 +1,20 @@
+# Config file for setup tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup.setup_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SetupTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
diff --git a/lab_system/master/config/tests/gating/setup_api_wifi.cfg b/lab_system/master/config/tests/gating/setup_api_wifi.cfg
new file mode 100644
index 0000000..1e014a4
--- /dev/null
+++ b/lab_system/master/config/tests/gating/setup_api_wifi.cfg
@@ -0,0 +1,19 @@
+# Config file for setup tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup_api_wifi.setup_api_wifi_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SetupApiWifiTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingCollectLogcatTestSequence
diff --git a/lab_system/master/config/tests/gating/setup_reboot.cfg b/lab_system/master/config/tests/gating/setup_reboot.cfg
new file mode 100644
index 0000000..bd43770
--- /dev/null
+++ b/lab_system/master/config/tests/gating/setup_reboot.cfg
@@ -0,0 +1,21 @@
+# Config file for setup reboot tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup.setup_reboot_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SetupRebootTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingCollectLogcatTestSequence
+
+
diff --git a/lab_system/master/config/tests/gating/volume.cfg b/lab_system/master/config/tests/gating/volume.cfg
new file mode 100644
index 0000000..7ed8b1f
--- /dev/null
+++ b/lab_system/master/config/tests/gating/volume.cfg
@@ -0,0 +1,17 @@
+# Config file for JavaScript video tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.volume.volume_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=VolumeTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+
diff --git a/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_paired.cfg b/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_paired.cfg
new file mode 100644
index 0000000..74cdc9a
--- /dev/null
+++ b/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_paired.cfg
@@ -0,0 +1,18 @@
+# Configuration values for NTI Contract Exec test.
+#
+# PROPERTIES
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PairedNTIContractTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.gqnq_contract_executor.nti_contract_exec_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NTIContractExecTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.gqnq_contract_executor.nti_contract_exec_sequence
+
+# Points to the folder containing JSON contracts in the GCS bucket
+# gs://cast-test-data/nti_contract_executor
+TEST_JSON_LOCATION=test_contracts/paired_tests
diff --git a/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_presubmit.cfg b/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_presubmit.cfg
new file mode 100644
index 0000000..bd52cb3
--- /dev/null
+++ b/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_presubmit.cfg
@@ -0,0 +1,18 @@
+# Configuration values for NTI Contract Exec test.
+#
+# PROPERTIES
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=NTIContractExecTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.gqnq_contract_executor.nti_contract_exec_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NTIContractExecTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.gqnq_contract_executor.nti_contract_exec_sequence
+
+# Points to the folder containing JSON contracts in the GCS bucket
+# gs://cast-test-data/nti_contract_executor
+TEST_JSON_LOCATION=test_contracts/presubmit_unpaired_tests
diff --git a/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_unpaired.cfg b/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_unpaired.cfg
new file mode 100644
index 0000000..a1e6e84
--- /dev/null
+++ b/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_unpaired.cfg
@@ -0,0 +1,18 @@
+# Configuration values for NTI Contract Exec test.
+#
+# PROPERTIES
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=UnpairedNTIContractTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.gqnq_contract_executor.nti_contract_exec_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NTIContractExecTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.gqnq_contract_executor.nti_contract_exec_sequence
+
+# Points to the folder containing JSON contracts in the GCS bucket
+# gs://cast-test-data/nti_contract_executor
+TEST_JSON_LOCATION=test_contracts/unpaired_tests
diff --git a/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_virtual_paired.cfg b/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_virtual_paired.cfg
new file mode 100644
index 0000000..b1d4498
--- /dev/null
+++ b/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_virtual_paired.cfg
@@ -0,0 +1,18 @@
+# Configuration values for Virtual NTI Contract Exec test.
+#
+# PROPERTIES
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PairedVirtualNTIContractTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.gqnq_contract_executor.nti_contract_exec_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NTIContractExecVirtualTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.gqnq_contract_executor.nti_contract_exec_sequence
+
+# Points to the folder containing JSON contracts in the GCS bucket
+# gs://cast-test-data/nti_contract_executor
+TEST_JSON_LOCATION=test_contracts/paired_tests
diff --git a/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_virtual_presubmit.cfg b/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_virtual_presubmit.cfg
new file mode 100644
index 0000000..ed4d6ef
--- /dev/null
+++ b/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_virtual_presubmit.cfg
@@ -0,0 +1,18 @@
+# Configuration values for Virtual NTI Contract Exec test.
+#
+# PROPERTIES
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=UnpairedVirtualNTIContractTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.gqnq_contract_executor.nti_contract_exec_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NTIContractExecVirtualTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.gqnq_contract_executor.nti_contract_exec_sequence
+
+# Points to the folder containing JSON contracts in the GCS bucket
+# gs://cast-test-data/nti_contract_executor
+TEST_JSON_LOCATION=test_contracts/presubmit_unpaired_tests
diff --git a/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_virtual_unpaired.cfg b/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_virtual_unpaired.cfg
new file mode 100644
index 0000000..857f5aa
--- /dev/null
+++ b/lab_system/master/config/tests/gqnq_contract_executor/gqnq_contract_executor_virtual_unpaired.cfg
@@ -0,0 +1,18 @@
+# Configuration values for Virtual NTI Contract Exec test.
+#
+# PROPERTIES
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=UnpairedVirtualNTIContractTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.gqnq_contract_executor.nti_contract_exec_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NTIContractExecVirtualTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.gqnq_contract_executor.nti_contract_exec_sequence
+
+# Points to the folder containing JSON contracts in the GCS bucket
+# gs://cast-test-data/nti_contract_executor
+TEST_JSON_LOCATION=test_contracts/unpaired_tests
diff --git a/lab_system/master/config/tests/hotmatch_samples_regression/hotmatch_samples_regression.cfg b/lab_system/master/config/tests/hotmatch_samples_regression/hotmatch_samples_regression.cfg
new file mode 100644
index 0000000..3d49662
--- /dev/null
+++ b/lab_system/master/config/tests/hotmatch_samples_regression/hotmatch_samples_regression.cfg
@@ -0,0 +1,40 @@
+# Config file for hotmatch samples regression on conducted audio.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotmatch_samples_regression.hotmatch_samples_regression_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotmatchSamplesRegressionTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotmatch_samples_regression.hotmatch_samples_regression_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotmatchSamplesRegressionTestSequence
+
+# PROPERTIES
+# Location of wav files on RPI
+HOTMATCH_SERVER_DIR=/home/pi/hotmatch_samples/server
+HOTMATCH_DEVICE_DIR=/home/pi/hotmatch_samples/device
+
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=HotmatchSamples
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Test suite name of test being run
+SUITE_NAME=HotmatchSamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 3.5 hrs including failures.
+SUITE_TIMEOUT_SECS=14400
+
+# Use Tunnel where applicable
+
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
diff --git a/lab_system/master/config/tests/hotword_detection_latency/hotword_detection_latency.cfg b/lab_system/master/config/tests/hotword_detection_latency/hotword_detection_latency.cfg
new file mode 100644
index 0000000..8b5697b
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_detection_latency/hotword_detection_latency.cfg
@@ -0,0 +1,22 @@
+# Config file for Hotword detection latency test on Chirp.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.e2e_latency.hotword_detection_latency_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordDetectionLatencyTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.e2e_latency.hotword_detection_latency_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordDetectionLatencyTestSequence
+
+# Use Tunnel where applicable
+
diff --git a/lab_system/master/config/tests/hotword_detection_latency/hotword_detection_latency_fuchsia.cfg b/lab_system/master/config/tests/hotword_detection_latency/hotword_detection_latency_fuchsia.cfg
new file mode 100644
index 0000000..6de2482
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_detection_latency/hotword_detection_latency_fuchsia.cfg
@@ -0,0 +1,22 @@
+# Config file for Hotword detection latency test on Chirp.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.e2e_latency.hotword_detection_latency_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordDetectionLatencyTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.e2e_latency.fuchsia_hotword_detection_latency_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaHotwordDetectionLatencyTestSequence
+
+# Use Tunnel where applicable
+
diff --git a/lab_system/master/config/tests/hotword_detection_latency/test_defs.json b/lab_system/master/config/tests/hotword_detection_latency/test_defs.json
new file mode 100644
index 0000000..7e76508
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_detection_latency/test_defs.json
@@ -0,0 +1,178 @@
+[
+  {
+    "group_name": "hotword_detection_latency",
+    "slave_name": "chirp_hotword_detection_latency_enclosures",
+    "test_name": "hotword_detection_latency",
+    "identifier": "chirp_hotword_detection_latency_master",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_e2e_enclosure",
+    "branch": "master",
+    "scheduler": "30 0,3,19 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hotword_detection_latency",
+    "slave_name": "chirp_hotword_detection_latency_enclosures",
+    "test_name": "hotword_detection_latency",
+    "identifier": "chirp_hotword_detection_latency_next_release",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_e2e_enclosure",
+    "branch": "next_release",
+    "scheduler": "30 1,4,20 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hotword_detection_latency",
+    "slave_name": "chirp_hotword_detection_latency_enclosures",
+    "test_name": "hotword_detection_latency",
+    "identifier": "chirp_hotword_detection_latency_release",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_e2e_enclosure",
+    "branch": "release",
+    "scheduler": "30 2,5,21 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hotword_detection_latency",
+    "slave_name": "joplin_hotword_detection_latency_enclosures",
+    "test_name": "hotword_detection_latency",
+    "identifier": "joplin_hotword_detection_latency_master",
+    "device_type": "joplin",
+    "device_group": "joplin_e2e_enclosure",
+    "branch": "master",
+    "scheduler": "30 0,3,19 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hotword_detection_latency",
+    "slave_name": "joplin_hotword_detection_latency_enclosures",
+    "test_name": "hotword_detection_latency",
+    "identifier": "joplin_hotword_detection_latency_next_release",
+    "device_type": "joplin",
+    "device_group": "joplin_e2e_enclosure",
+    "branch": "1_28a",
+    "scheduler": "30 1,4,20 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hotword_detection_latency",
+    "slave_name": "joplin_hotword_detection_latency_enclosures",
+    "test_name": "hotword_detection_latency",
+    "identifier": "joplin_hotword_detection_latency_release",
+    "device_type": "joplin",
+    "device_group": "joplin_e2e_enclosure",
+    "branch": "release",
+    "scheduler": "30 2,5,21 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hotword_detection_latency",
+    "slave_name": "biggie_hotword_detection_latency_enclosures",
+    "test_name": "hotword_detection_latency",
+    "identifier": "biggie_hotword_detection_latency_master",
+    "device_type": "biggie",
+    "device_group": "biggie_e2e_enclosure",
+    "branch": "master",
+    "scheduler": "30 0,3,19 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hotword_detection_latency",
+    "slave_name": "biggie_hotword_detection_latency_enclosures",
+    "test_name": "hotword_detection_latency",
+    "identifier": "biggie_hotword_detection_latency_next_release",
+    "device_type": "biggie",
+    "device_group": "biggie_e2e_enclosure",
+    "branch": "next_release",
+    "scheduler": "30 1,4,20 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/tests/hotword_samples_regression/hotword_samples_conducted_audio_regression.cfg b/lab_system/master/config/tests/hotword_samples_regression/hotword_samples_conducted_audio_regression.cfg
new file mode 100644
index 0000000..d26ef30
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression/hotword_samples_conducted_audio_regression.cfg
@@ -0,0 +1,35 @@
+# Config file for hotword samples regression for conducted audio.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=OkGoogleSamples
+
+# Location of wav files on RPI.
+RPI_AUDIO_SAMPLES_DIR=/home/pi/ok_google_samples
+
+# Test suite name of test being run.
+SUITE_NAME=HotwordCASamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 3.5 hrs including failures.
+SUITE_TIMEOUT_SECS=14400
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSamplesRegressionTestSequence
+
+# Use Tunnel where applicable
+
+TCP_TUNNEL_TIMEOUT_SECS=1800
diff --git a/lab_system/master/config/tests/hotword_samples_regression/hotword_samples_regression.cfg b/lab_system/master/config/tests/hotword_samples_regression/hotword_samples_regression.cfg
new file mode 100644
index 0000000..8e2f8b9
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression/hotword_samples_regression.cfg
@@ -0,0 +1,41 @@
+# Config file for hotword samples regression on Chirp in the enclosure.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Hotword speaker volume
+HW_SPEAKER_VOL=94
+
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=OkGoogleSamples
+
+# Location of wav files on RPI
+RPI_AUDIO_SAMPLES_DIR=/home/pi/ok_google_samples
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Test suite name of test being run
+SUITE_NAME=HotwordSamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 3.5 hrs including failures.
+SUITE_TIMEOUT_SECS=14400
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSamplesRegressionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_sequence
\ No newline at end of file
diff --git a/lab_system/master/config/tests/hotword_samples_regression/hotword_samples_regression_fuchsia.cfg b/lab_system/master/config/tests/hotword_samples_regression/hotword_samples_regression_fuchsia.cfg
new file mode 100644
index 0000000..01eb762
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression/hotword_samples_regression_fuchsia.cfg
@@ -0,0 +1,38 @@
+# Config file for hotword samples regression on Chirp in the enclosure.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Hotword speaker volume
+HW_SPEAKER_VOL=94
+
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=OkGoogleSamples
+
+# Location of wav files on RPI
+RPI_AUDIO_SAMPLES_DIR=/home/pi/ok_google_samples
+
+# Test suite name of test being run
+SUITE_NAME=HotwordSamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 3.5 hrs including failures.
+SUITE_TIMEOUT_SECS=14400
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaHotwordSamplesRegressionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.fuchsia_hotword_samples_regression_test_sequence
diff --git a/lab_system/master/config/tests/hotword_samples_regression/noisy_hotword_samples_regression.cfg b/lab_system/master/config/tests/hotword_samples_regression/noisy_hotword_samples_regression.cfg
new file mode 100644
index 0000000..662e690
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression/noisy_hotword_samples_regression.cfg
@@ -0,0 +1,47 @@
+# Config file for testing hotword samples with noise.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Hotword speaker volume
+HW_SPEAKER_VOL=94
+
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=NoisySamples
+
+# Location of wav files on RPI
+RPI_AUDIO_SAMPLES_DIR=/home/pi/art_query_samples
+
+# Location of noise files on RPI
+RPI_NOISE_DIR=/home/pi/art_noise_samples
+
+# Noise gain to apply to noise samples
+NOISE_GAIN=0.2
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Test suite name of test being run
+SUITE_NAME=HotwordSamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 3.5 hrs including failures.
+SUITE_TIMEOUT_SECS=14400
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSamplesRegressionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_sequence
\ No newline at end of file
diff --git a/lab_system/master/config/tests/hotword_samples_regression/test_defs.json b/lab_system/master/config/tests/hotword_samples_regression/test_defs.json
new file mode 100644
index 0000000..3f6128b
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression/test_defs.json
@@ -0,0 +1,72 @@
+[
+  {
+    "group_name": "hotword_samples_regression",
+    "slave_name": "chirp_eraser_enclosure",
+    "test_name": "hotword_samples_regression",
+    "identifier": "chirp_hotword_samples_regression_master",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_hotword_samples_enclosure",
+    "branch": "master",
+    "scheduler": "0 2 1,4,7,10,13,16,19,22,25,28,31 * *"
+  },
+  {
+    "group_name": "hotword_samples_regression",
+    "slave_name": "chirp_eraser_enclosure",
+    "test_name": "hotword_samples_regression",
+    "identifier": "chirp_hotword_samples_regression_next_release",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_hotword_samples_enclosure",
+    "branch": "next_release",
+    "scheduler": "0 2 2,5,8,11,14,17,20,23,26,29 * *"
+  },
+  {
+    "group_name": "hotword_samples_regression",
+    "slave_name": "chirp_eraser_enclosure",
+    "test_name": "hotword_samples_regression",
+    "identifier": "chirp_hotword_samples_regression_release",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_hotword_samples_enclosure",
+    "branch": "release",
+    "scheduler": "0 2 3,6,9,12,15,18,21,24,27,30 * *"
+  },
+  {
+    "group_name": "hotword_samples_regression",
+    "slave_name": "joplin_eraser_enclosure",
+    "test_name": "hotword_samples_regression",
+    "identifier": "joplin_hotword_samples_regression_master",
+    "device_type": "joplin",
+    "device_group": "joplin_hotword_samples_enclosure",
+    "branch": "master",
+    "scheduler": "0 2 1,5,9,13,17,21,25,29 * *"
+  },
+  {
+    "group_name": "hotword_samples_regression",
+    "slave_name": "joplin_eraser_enclosure",
+    "test_name": "hotword_samples_regression",
+    "identifier": "joplin_hotword_samples_regression_next_release",
+    "device_type": "joplin",
+    "device_group": "joplin_hotword_samples_enclosure",
+    "branch": "1_28a",
+    "scheduler": "0 2 2,4,6,8,10,12,14,16,18,20,22,24,26,28,30 * *"
+  },
+  {
+    "group_name": "hotword_samples_regression",
+    "slave_name": "biggie_eraser_enclosure",
+    "test_name": "hotword_samples_regression",
+    "identifier": "biggie_hotword_samples_regression_master",
+    "device_type": "biggie_b3",
+    "device_group": "biggie_hotword_samples_enclosure",
+    "branch": "master",
+    "scheduler": "0 2 1,5,9,13,17,21,25,29 * *"
+  },
+  {
+    "group_name": "hotword_samples_regression",
+    "slave_name": "biggie_eraser_enclosure",
+    "test_name": "hotword_samples_regression",
+    "identifier": "biggie_hotword_samples_regression_next_release",
+    "device_type": "biggie_b3",
+    "device_group": "biggie_hotword_samples_enclosure",
+    "branch": "next_release",
+    "scheduler": "0 2 2,4,6,8,10,12,14,16,18,20,22,24,26,28,30 * *"
+  }
+]
diff --git a/lab_system/master/config/tests/hotword_samples_regression_art_clean/hotword_samples_regression_art_clean.cfg b/lab_system/master/config/tests/hotword_samples_regression_art_clean/hotword_samples_regression_art_clean.cfg
new file mode 100755
index 0000000..94c13a8
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression_art_clean/hotword_samples_regression_art_clean.cfg
@@ -0,0 +1,38 @@
+# Config file for hotword samples regression on Chirp in the enclosure.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Location of wav files on RPI.
+RPI_AUDIO_SAMPLES_DIR=/home/pi/hotword_art_clean_samples
+
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=ArtCleanSamples
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Test suite name of test being run.
+SUITE_NAME=HotwordSamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 8 hrs including failures.
+SUITE_TIMEOUT_SECS=43800
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSamplesRegressionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_sequence
\ No newline at end of file
diff --git a/lab_system/master/config/tests/hotword_samples_regression_art_clean/hotword_samples_regression_art_clean_fuchsia.cfg b/lab_system/master/config/tests/hotword_samples_regression_art_clean/hotword_samples_regression_art_clean_fuchsia.cfg
new file mode 100755
index 0000000..45f7257
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression_art_clean/hotword_samples_regression_art_clean_fuchsia.cfg
@@ -0,0 +1,35 @@
+# Config file for hotword samples regression on Chirp in the enclosure.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Location of wav files on RPI.
+RPI_AUDIO_SAMPLES_DIR=/home/pi/hotword_art_clean_samples
+
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=ArtCleanSamples
+
+# Test suite name of test being run.
+SUITE_NAME=HotwordSamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 8 hrs including failures.
+SUITE_TIMEOUT_SECS=43800
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaHotwordSamplesRegressionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.fuchsia_hotword_samples_regression_test_sequence
diff --git a/lab_system/master/config/tests/hotword_samples_regression_artmusic/hotword_samples_conducted_audio_regression_artmusic.cfg b/lab_system/master/config/tests/hotword_samples_regression_artmusic/hotword_samples_conducted_audio_regression_artmusic.cfg
new file mode 100644
index 0000000..6e46146
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression_artmusic/hotword_samples_conducted_audio_regression_artmusic.cfg
@@ -0,0 +1,35 @@
+# Config file for hotword samples regression for conducted audio.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=ARTMixedWithMusic
+
+# Location of wav files on RPI.
+RPI_AUDIO_SAMPLES_DIR=/home/pi/art_mixed_music
+
+# Test suite name of test being run.
+SUITE_NAME=HotwordSamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 8 hrs including failures.
+SUITE_TIMEOUT_SECS=28800
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSamplesRegressionTestSequence
+
+# Use Tunnel where applicable
+
+TCP_TUNNEL_TIMEOUT_SECS=1800
diff --git a/lab_system/master/config/tests/hotword_samples_regression_artmusic/hotword_samples_regression_artmusic.cfg b/lab_system/master/config/tests/hotword_samples_regression_artmusic/hotword_samples_regression_artmusic.cfg
new file mode 100644
index 0000000..e72cb47
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression_artmusic/hotword_samples_regression_artmusic.cfg
@@ -0,0 +1,35 @@
+# Config file for hotword samples regression on Chirp in the enclosure.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=ARTMixedWithMusic
+
+# Location of wav files on RPI.
+RPI_AUDIO_SAMPLES_DIR=/home/pi/art_mixed_music
+
+# Test suite name of test being run.
+SUITE_NAME=HotwordSamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 8 hrs including failures.
+SUITE_TIMEOUT_SECS=28800
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSamplesRegressionTestSequence
+
+# Use Tunnel where applicable
+
+TCP_TUNNEL_TIMEOUT_SECS=1800
diff --git a/lab_system/master/config/tests/hotword_samples_regression_clean/hotword_samples_conducted_audio_regression_clean.cfg b/lab_system/master/config/tests/hotword_samples_regression_clean/hotword_samples_conducted_audio_regression_clean.cfg
new file mode 100644
index 0000000..9dacca7
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression_clean/hotword_samples_conducted_audio_regression_clean.cfg
@@ -0,0 +1,35 @@
+# Config file for hotword samples regression for conducted audio.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=CleanSamples
+
+# Location of wav files on RPI.
+RPI_AUDIO_SAMPLES_DIR=/home/pi/clean_audio_hotword_samples
+
+# Test suite name of test being run.
+SUITE_NAME=HotwordCASamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 0.5 hrs including failures.
+SUITE_TIMEOUT_SECS=1800
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSamplesRegressionTestSequence
+
+# Use Tunnel where applicable
+
+TCP_TUNNEL_TIMEOUT_SECS=1800
diff --git a/lab_system/master/config/tests/hotword_samples_regression_clean/hotword_samples_regression_clean.cfg b/lab_system/master/config/tests/hotword_samples_regression_clean/hotword_samples_regression_clean.cfg
new file mode 100644
index 0000000..92ac71c
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression_clean/hotword_samples_regression_clean.cfg
@@ -0,0 +1,35 @@
+# Config file for hotword samples regression on Chirp in the enclosure.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=CleanSamples
+
+# Location of wav files on RPI
+RPI_AUDIO_SAMPLES_DIR=/home/pi/clean_audio_hotword_samples
+
+# Test suite name of test being run
+SUITE_NAME=HotwordSamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 0.5 hrs including failures.
+SUITE_TIMEOUT_SECS=1800
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSamplesRegressionTestSequence
+
+# Use Tunnel where applicable
+
+TCP_TUNNEL_TIMEOUT_SECS=1800
diff --git a/lab_system/master/config/tests/hotword_samples_regression_heyg/hotword_samples_conducted_audio_regression_heyg.cfg b/lab_system/master/config/tests/hotword_samples_regression_heyg/hotword_samples_conducted_audio_regression_heyg.cfg
new file mode 100644
index 0000000..ab22a18
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression_heyg/hotword_samples_conducted_audio_regression_heyg.cfg
@@ -0,0 +1,35 @@
+# Config file for hotword samples regression for conducted audio.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=HeyGoogleSamples
+
+# Location of wav files on RPI.
+RPI_AUDIO_SAMPLES_DIR=/home/pi/hey_google_samples
+
+# Test suite name of test being run.
+SUITE_NAME=HotwordCASamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 11.5 hrs including failures.
+SUITE_TIMEOUT_SECS=43200
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSamplesRegressionTestSequence
+
+# Use Tunnel where applicable
+
+TCP_TUNNEL_TIMEOUT_SECS=1800
diff --git a/lab_system/master/config/tests/hotword_samples_regression_heyg/hotword_samples_regression_heyg.cfg b/lab_system/master/config/tests/hotword_samples_regression_heyg/hotword_samples_regression_heyg.cfg
new file mode 100644
index 0000000..4d85ddc
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression_heyg/hotword_samples_regression_heyg.cfg
@@ -0,0 +1,41 @@
+# Config file for hotword samples regression on Chirp in the enclosure.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Hotword speaker volume
+HW_SPEAKER_VOL=94
+
+# Location of wav files on RPI.
+RPI_AUDIO_SAMPLES_DIR=/home/pi/hey_google_samples
+
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=HeyGoogleSamples
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Test suite name of test being run.
+SUITE_NAME=HotwordSamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 11.5 hrs including failures.
+SUITE_TIMEOUT_SECS=43200
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSamplesRegressionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_sequence
\ No newline at end of file
diff --git a/lab_system/master/config/tests/hotword_samples_regression_heyg/hotword_samples_regression_heyg_fuchsia.cfg b/lab_system/master/config/tests/hotword_samples_regression_heyg/hotword_samples_regression_heyg_fuchsia.cfg
new file mode 100644
index 0000000..d7abb29
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression_heyg/hotword_samples_regression_heyg_fuchsia.cfg
@@ -0,0 +1,38 @@
+# Config file for hotword samples regression on Chirp in the enclosure.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Hotword speaker volume
+HW_SPEAKER_VOL=94
+
+# Location of wav files on RPI.
+RPI_AUDIO_SAMPLES_DIR=/home/pi/hey_google_samples
+
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=HeyGoogleSamples
+
+# Test suite name of test being run.
+SUITE_NAME=HotwordSamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 11.5 hrs including failures.
+SUITE_TIMEOUT_SECS=43200
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaHotwordSamplesRegressionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.fuchsia_hotword_samples_regression_test_sequence
diff --git a/lab_system/master/config/tests/hotword_samples_regression_noisy/hotword_samples_regression_no_noise.cfg b/lab_system/master/config/tests/hotword_samples_regression_noisy/hotword_samples_regression_no_noise.cfg
new file mode 100644
index 0000000..6f5cc2d
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression_noisy/hotword_samples_regression_no_noise.cfg
@@ -0,0 +1,41 @@
+# Config file for testing noisy hotword samples without noise for baselining.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Hotword speaker volume
+HW_SPEAKER_VOL=94
+
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=NoisySamples
+
+# Location of wav files on RPI
+RPI_AUDIO_SAMPLES_DIR=/home/pi/art_query_samples
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Test suite name of test being run
+SUITE_NAME=HotwordSamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 3.5 hrs including failures.
+SUITE_TIMEOUT_SECS=14400
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSamplesRegressionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_sequence
\ No newline at end of file
diff --git a/lab_system/master/config/tests/hotword_samples_regression_noisy/hotword_samples_regression_noisy.cfg b/lab_system/master/config/tests/hotword_samples_regression_noisy/hotword_samples_regression_noisy.cfg
new file mode 100644
index 0000000..662e690
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression_noisy/hotword_samples_regression_noisy.cfg
@@ -0,0 +1,47 @@
+# Config file for testing hotword samples with noise.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Hotword speaker volume
+HW_SPEAKER_VOL=94
+
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=NoisySamples
+
+# Location of wav files on RPI
+RPI_AUDIO_SAMPLES_DIR=/home/pi/art_query_samples
+
+# Location of noise files on RPI
+RPI_NOISE_DIR=/home/pi/art_noise_samples
+
+# Noise gain to apply to noise samples
+NOISE_GAIN=0.2
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Test suite name of test being run
+SUITE_NAME=HotwordSamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 3.5 hrs including failures.
+SUITE_TIMEOUT_SECS=14400
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSamplesRegressionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_sequence
\ No newline at end of file
diff --git a/lab_system/master/config/tests/hotword_samples_regression_noisy/hotword_samples_regression_noisy_fuchsia.cfg b/lab_system/master/config/tests/hotword_samples_regression_noisy/hotword_samples_regression_noisy_fuchsia.cfg
new file mode 100644
index 0000000..d25d2c3
--- /dev/null
+++ b/lab_system/master/config/tests/hotword_samples_regression_noisy/hotword_samples_regression_noisy_fuchsia.cfg
@@ -0,0 +1,47 @@
+# Config file for testing hotword samples with noise.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Hotword speaker volume
+HW_SPEAKER_VOL=94
+
+# Type of Hotword samples being tested.
+SAMPLE_TYPE=NoisySamples
+
+# Location of wav files on RPI
+RPI_AUDIO_SAMPLES_DIR=/home/pi/art_query_samples
+
+# Location of noise files on RPI
+RPI_NOISE_DIR=/home/pi/art_noise_samples
+
+# Noise gain to apply to noise samples
+NOISE_GAIN=0.2
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Test suite name of test being run
+SUITE_NAME=HotwordSamplesRegression
+
+# Timeout for complete test run.
+# Currently, it takes 3.5 hrs including failures.
+SUITE_TIMEOUT_SECS=14400
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSamplesTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotword_samples_regression.hotword_samples_regression_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaHotwordSamplesRegressionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotword_samples_regression.fuchsia_hotword_samples_regression_test_sequence
diff --git a/lab_system/master/config/tests/hotwording/hotword_sensitivity_CA.cfg b/lab_system/master/config/tests/hotwording/hotword_sensitivity_CA.cfg
new file mode 100644
index 0000000..476811d
--- /dev/null
+++ b/lab_system/master/config/tests/hotwording/hotword_sensitivity_CA.cfg
@@ -0,0 +1,26 @@
+# Config file for Hotword Sensitivity on Nest Assistant devices
+#
+# First four fields should always be present
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotwording.hotword_sensitivity_test_case
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSensitivityTestCase
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotwording.hotword_sensitivity_test_sequence
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSensitivityTestSequence
+
+# All other test-specific fields should be placed in alphabetical order after
+# Location of hotword samples
+GCS_AUDIO_DIR_PATH=assistant/hotword_samples_silence_2_channels
+
+# Level at which to mix pink noise with utterances
+NOISE_LEVEL=.000001
+
+# Options are 'S', 'M', 'L' (Small, Medium, Large)
+# For debugging, can instead use a partial match string (e.g. 'speaker12', 'hey_google', etc.)
+RUN_SIZE=M
+
+# Options are 'Jabberjay', 'ConductedAudio', and 'Chatterbox'
+STIMULUS_TYPE=ConductedAudio
\ No newline at end of file
diff --git a/lab_system/master/config/tests/hotwording/hotword_sensitivity_jabberjay.cfg b/lab_system/master/config/tests/hotwording/hotword_sensitivity_jabberjay.cfg
new file mode 100644
index 0000000..41a5657
--- /dev/null
+++ b/lab_system/master/config/tests/hotwording/hotword_sensitivity_jabberjay.cfg
@@ -0,0 +1,26 @@
+# Config file for Hotword Sensitivity on Nest Assistant devices
+#
+# First four fields should always be present
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotwording.hotword_sensitivity_test_case
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSensitivityTestCase
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotwording.hotword_sensitivity_test_sequence
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSensitivityTestSequence
+
+# All other test-specific fields should be placed in alphabetical order after
+# Location of hotword samples
+GCS_AUDIO_DIR_PATH=assistant/assistant_hotword_sample_recordings_added_silence
+
+# Level at which to mix pink noise with utterances
+NOISE_LEVEL=.001
+
+# Options are 'S', 'M', 'L' (Small, Medium, Large)
+# For debugging, can instead use a partial match string (e.g. 'speaker12', 'hey_google', etc.)
+RUN_SIZE=L
+
+# Options are 'Jabberjay', 'ConductedAudio', and 'Chatterbox'
+STIMULUS_TYPE=Jabberjay
\ No newline at end of file
diff --git a/lab_system/master/config/tests/hotwording/hotword_sensitivity_jabberjay_fuchsia.cfg b/lab_system/master/config/tests/hotwording/hotword_sensitivity_jabberjay_fuchsia.cfg
new file mode 100644
index 0000000..611a854
--- /dev/null
+++ b/lab_system/master/config/tests/hotwording/hotword_sensitivity_jabberjay_fuchsia.cfg
@@ -0,0 +1,26 @@
+# Config file for Hotword Sensitivity on Nest Assistant devices
+#
+# First four fields should always be present
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotwording.hotword_sensitivity_test_case
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSensitivityTestCase
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotwording.fuchsia_hotword_sensitivity_test_sequence
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaHotwordSensitivityTestSequence
+
+# All other test-specific fields should be placed in alphabetical order after
+# Location of hotword samples
+GCS_AUDIO_DIR_PATH=assistant/assistant_hotword_sample_recordings_added_silence
+
+# Level at which to mix pink noise with utterances
+NOISE_LEVEL=.001
+
+# Options are 'S', 'M', 'L' (Small, Medium, Large)
+# For debugging, can instead use a partial match string (e.g. 'speaker12', 'hey_google', etc.)
+RUN_SIZE=L
+
+# Options are 'Jabberjay', 'ConductedAudio', and 'Chatterbox'
+STIMULUS_TYPE=Jabberjay
\ No newline at end of file
diff --git a/lab_system/master/config/tests/hotwording/hotword_sensitivity_levels.cfg b/lab_system/master/config/tests/hotwording/hotword_sensitivity_levels.cfg
new file mode 100644
index 0000000..01c0dc1
--- /dev/null
+++ b/lab_system/master/config/tests/hotwording/hotword_sensitivity_levels.cfg
@@ -0,0 +1,32 @@
+# Config file for Hotword Sensitivity on Nest Assistant devices
+#
+# First four fields should always be present
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotwording.hotword_sensitivity_test_case
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordSensitivityLevelsTestCase
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotwording.hotword_sensitivity_test_sequence
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordSensitivityTestSequence
+
+# All other test-specific fields should be placed in alphabetical order after
+# Location of hotword samples
+GCS_AUDIO_DIR_PATH=assistant/hotwording/hotword_sensitivity_levels_samples
+
+# Level at which to mix pink noise with utterances
+NOISE_LEVEL=.001
+
+# If a custom Range is used for a test, it can be optionally be defined here
+RANGE=.003, .004
+
+# Options are 'S', 'M', 'L' (Small, Medium, Large)
+# For debugging, can instead use a partial match string (e.g. 'speaker12', 'hey_google', etc.)
+RUN_SIZE=L
+
+# These fields are defined in the HwSensitivity enum in device_attribute_helper
+SENSITIVITY_LEVEL=1,2,3,4,5
+
+# Options are 'Jabberjay', 'ConductedAudio', and 'Chatterbox'
+STIMULUS_TYPE=Jabberjay
\ No newline at end of file
diff --git a/lab_system/master/config/tests/hotwording/hotwording.cfg b/lab_system/master/config/tests/hotwording/hotwording.cfg
new file mode 100644
index 0000000..967f5bc
--- /dev/null
+++ b/lab_system/master/config/tests/hotwording/hotwording.cfg
@@ -0,0 +1,22 @@
+# Config file for Hotwording  on Joplin.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hotwording.hotwording_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=HotwordingTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hotwording.hotwording_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HotwordingTestSequence
+
+# Use Tunnel where applicable
+
diff --git a/lab_system/master/config/tests/hotwording/test_defs.json b/lab_system/master/config/tests/hotwording/test_defs.json
new file mode 100644
index 0000000..c5d6a2d
--- /dev/null
+++ b/lab_system/master/config/tests/hotwording/test_defs.json
@@ -0,0 +1,178 @@
+[
+  {
+    "group_name": "hotwording",
+    "slave_name": "chirp_hotwording_enclosures",
+    "test_name": "hotwording",
+    "identifier": "chirp_hotwording_master",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_e2e_enclosure",
+    "branch": "master",
+    "scheduler": "0 3,12,15 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hotwording",
+    "slave_name": "chirp_hotwording_enclosures",
+    "test_name": "hotwording",
+    "identifier": "chirp_hotwording_next_release",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_e2e_enclosure",
+    "branch": "next_release",
+    "scheduler": "0 4,13,16 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hotwording",
+    "slave_name": "chirp_hotwording_enclosures",
+    "test_name": "hotwording",
+    "identifier": "chirp_hotwording_release",
+    "device_type": "chirp_b3",
+    "device_group": "chirp_e2e_enclosure",
+    "branch": "release",
+    "scheduler": "0 5,14,17 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hotwording",
+    "slave_name": "joplin_hotwording_enclosures",
+    "test_name": "hotwording",
+    "identifier": "joplin_hotwording_master",
+    "device_type": "joplin",
+    "device_group": "joplin_e2e_enclosure",
+    "branch": "master",
+    "scheduler": "0 3,12,15 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hotwording",
+    "slave_name": "joplin_hotwording_enclosures",
+    "test_name": "hotwording",
+    "identifier": "joplin_hotwording_next_release",
+    "device_type": "joplin",
+    "device_group": "joplin_e2e_enclosure",
+    "branch": "1_28a",
+    "scheduler": "0 4,13,16 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hotwording",
+    "slave_name": "joplin_hotwording_enclosures",
+    "test_name": "hotwording",
+    "identifier": "joplin_hotwording_release",
+    "device_type": "joplin",
+    "device_group": "joplin_e2e_enclosure",
+    "branch": "release",
+    "scheduler": "0 5,14,17 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hotwording",
+    "slave_name": "biggie_hotwording_enclosures",
+    "test_name": "hotwording",
+    "identifier": "biggie_hotwording_master",
+    "device_type": "biggie",
+    "device_group": "biggie_e2e_enclosure",
+    "branch": "master",
+    "scheduler": "0 3,12,15 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "hotwording",
+    "slave_name": "biggie_hotwording_enclosures",
+    "test_name": "hotwording",
+    "identifier": "biggie_hotwording_next_release",
+    "device_type": "biggie",
+    "device_group": "biggie_e2e_enclosure",
+    "branch": "next_release",
+    "scheduler": "0 4,13,16 * * *",
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "TwoHourLeaseShortTimeoutAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/tests/hs3_assistant/hs3_assistant_test.cfg b/lab_system/master/config/tests/hs3_assistant/hs3_assistant_test.cfg
new file mode 100644
index 0000000..5a09d15
--- /dev/null
+++ b/lab_system/master/config/tests/hs3_assistant/hs3_assistant_test.cfg
@@ -0,0 +1,17 @@
+# Config file for hermetic S3 smoke tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.assistant.hs3_assistant_test.hs3_assistant_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=Hs3AssistantTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.hs3_assistant_test.hs3_assistant_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=Hs3AssistantTestSequence
+
+BUILD_SELECTION_POLICY=backtrack_then_latest
diff --git a/lab_system/master/config/tests/imax/imax.cfg b/lab_system/master/config/tests/imax/imax.cfg
new file mode 100644
index 0000000..d047b2e
--- /dev/null
+++ b/lab_system/master/config/tests/imax/imax.cfg
@@ -0,0 +1,27 @@
+# Config file for the imax tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.imax.imax_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ImaxTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.imax.imax_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=ImaxTestSequence
+
+# The server to use for Imax Actions
+SERVER_URL=chromecast-alpha.sandbox.google.com
+
+# App config path for the given server.
+APP_CONFIG_PATH=chrome://home?remote_url=https%3A%2F%2Fchromecast-alpha.sandbox.google.com%2Fcast%2Fchromecast%2Fhome%3Fclient-logging-level%3Dall%26slideshow-period%3D1
+
+# Gaia ID for prod test account
+GAIA_ID=473911208807
diff --git a/lab_system/master/config/tests/imax/imax_prod.cfg b/lab_system/master/config/tests/imax/imax_prod.cfg
new file mode 100644
index 0000000..84bc708
--- /dev/null
+++ b/lab_system/master/config/tests/imax/imax_prod.cfg
@@ -0,0 +1,27 @@
+# Config file for the imax tests in production environment
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.imax.imax_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ImaxTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.imax.imax_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=ImaxTestSequence
+
+# The server to use for Imax Actions
+SERVER_URL=clients3.google.com
+
+# App config path for the given server.
+APP_CONFIG_PATH=chrome://home?remote_url=https%3A%2F%2Fclients3.google.com%2Fcast%2Fchromecast%2Fhome%3Fclient-logging-level%3Dall%26slideshow-period%3D1
+
+# Gaia ID for prod test account
+GAIA_ID=473911208807
diff --git a/lab_system/master/config/tests/imax/imax_staging.cfg b/lab_system/master/config/tests/imax/imax_staging.cfg
new file mode 100644
index 0000000..43981a7
--- /dev/null
+++ b/lab_system/master/config/tests/imax/imax_staging.cfg
@@ -0,0 +1,27 @@
+# Config file for the imax tests for staging environment
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.imax.imax_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ImaxTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.imax.imax_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=ImaxTestSequence
+
+# The server to use for Imax Actions
+SERVER_URL=chromecast-staging.sandbox.google.com
+
+# App config path for the given server.
+APP_CONFIG_PATH=chrome://home?remote_url=https%3A%2F%2Fchromecast-staging.sandbox.google.com%2Fcast%2Fchromecast%2Fhome%3Fclient-logging-level%3Dall%26slideshow-period%3D1
+
+# Gaia ID for prod test account
+GAIA_ID=473911208807
diff --git a/lab_system/master/config/tests/imax/test_defs.json b/lab_system/master/config/tests/imax/test_defs.json
new file mode 100644
index 0000000..3be4eda
--- /dev/null
+++ b/lab_system/master/config/tests/imax/test_defs.json
@@ -0,0 +1,80 @@
+[
+    {
+        "branch": [
+            "master",
+            "release"
+        ],
+        "device_group": "eureka_debug",
+        "device_type": "eureka",
+        "group_name": "imax",
+        "identifier": "imax_staging",
+        "slave_name": "imax",
+        "hidden": "true",
+        "test_name": "imax_staging"
+    },
+    {
+        "branch": [
+            "master",
+            "release"
+        ],
+        "device_group": "eureka_debug",
+        "device_type": "eureka",
+        "group_name": "imax",
+        "identifier": "imax",
+        "slave_name": "imax",
+        "hidden": "true",
+        "test_name": "imax"
+    },
+    {
+        "branch": [
+            "master",
+            "release"
+        ],
+        "device_group": "eureka_debug",
+        "device_type": "eureka",
+        "group_name": "imax",
+        "identifier": "imax_prod",
+        "slave_name": "imax",
+        "hidden": "true",
+        "test_name": "imax_prod"
+    },
+    {
+        "branch": [
+            "master",
+            "release"
+        ],
+        "device_group": "lexx_eth",
+        "device_type": "lexx",
+        "group_name": "imax",
+        "identifier": "imax_staging_v2",
+        "slave_name": "imax",
+        "hidden": "true",
+        "test_name": "imax_staging"
+    },
+    {
+        "branch": [
+            "master",
+            "release"
+        ],
+        "device_group": "lexx_eth",
+        "device_type": "lexx",
+        "group_name": "imax",
+        "identifier": "imax_v2",
+        "slave_name": "imax",
+        "hidden": "true",
+        "test_name": "imax"
+    },
+    {
+        "branch": [
+            "master",
+            "release"
+        ],
+        "device_group": "lexx_eth",
+        "device_type": "lexx",
+        "group_name": "imax",
+        "identifier": "imax_prod_v2",
+        "slave_name": "imax",
+        "hidden": "true",
+        "test_name": "imax_prod"
+    }
+]
diff --git a/lab_system/master/config/tests/js_features/js_features.cfg b/lab_system/master/config/tests/js_features/js_features.cfg
new file mode 100644
index 0000000..a91c244
--- /dev/null
+++ b/lab_system/master/config/tests/js_features/js_features.cfg
@@ -0,0 +1,16 @@
+# Config file for testing enabled/disabled JavaScript app features.
+
+# Type of test used by the fr cli tool to run javascript tests:
+TEST_TYPE=javascript
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.js_features.js_features_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FeaturesEnabledTestCase,FeaturesDisabledTestCase
+
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.js_features.js_features_test_sequence
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JsFeaturesTestSequence
diff --git a/lab_system/master/config/tests/locale_string/locale_string.cfg b/lab_system/master/config/tests/locale_string/locale_string.cfg
new file mode 100644
index 0000000..d67fc2f
--- /dev/null
+++ b/lab_system/master/config/tests/locale_string/locale_string.cfg
@@ -0,0 +1,15 @@
+# Config file for JavaScript audio format tests.
+#
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.locale_string.locale_string_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=BasicStringTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+
diff --git a/lab_system/master/config/tests/long_audio_play/long_audio_play.cfg b/lab_system/master/config/tests/long_audio_play/long_audio_play.cfg
new file mode 100644
index 0000000..faff9d0
--- /dev/null
+++ b/lab_system/master/config/tests/long_audio_play/long_audio_play.cfg
@@ -0,0 +1,18 @@
+# Config file for testing RebootLoop.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.long_audio_play.long_audio_play_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=LongAudioPlayTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
+
+# Plot data
+PLOT=False
\ No newline at end of file
diff --git a/lab_system/master/config/tests/magic_mic_test/magic_mic_test.cfg b/lab_system/master/config/tests/magic_mic_test/magic_mic_test.cfg
new file mode 100644
index 0000000..8cdcb08
--- /dev/null
+++ b/lab_system/master/config/tests/magic_mic_test/magic_mic_test.cfg
@@ -0,0 +1,13 @@
+# Config file for magic mic query tests
+
+TEST_CASE_MODULE=eurtest.tests.assistant.magic_mic_test_case
+
+TEST_CASE_CLASS=MagicMicTest
+
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.assistant_test_sequence
+
+TEST_SEQUENCE_CLASS=AssistantTestSequence
+
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/magic_mic
+
+ASSISTANT_BACKEND=DOGFOOD_SERVER
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_ambient.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_ambient.cfg
new file mode 100644
index 0000000..eece3eb
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_ambient.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan Ambient UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.ambient_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AmbientTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_animations.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_animations.cfg
new file mode 100644
index 0000000..919253f
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_animations.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan Animations UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.animations_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AnimationsTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_back_gestures.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_back_gestures.cfg
new file mode 100644
index 0000000..1f9cc10
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_back_gestures.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan Back Gestures UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.back_gestures_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=BackGesturesTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_carousel_load.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_carousel_load.cfg
new file mode 100644
index 0000000..7b09d6b
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_carousel_load.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan Carousel UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.carousel_loading_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CarouselLoadingTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_carousel_scroll.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_carousel_scroll.cfg
new file mode 100644
index 0000000..fa154d0
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_carousel_scroll.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan Carousel UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.carousel_scrolling_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CarouselScrollingTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_coin_tray_gestures.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_coin_tray_gestures.cfg
new file mode 100644
index 0000000..4625054
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_coin_tray_gestures.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan System Gestures UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.coin_tray_gestures_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CoinTrayGesturesTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_flutter_bridge_screenshot.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_flutter_bridge_screenshot.cfg
new file mode 100644
index 0000000..f866a27
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_flutter_bridge_screenshot.cfg
@@ -0,0 +1,15 @@
+# Config file for Opal UI screenshot test.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.flutter_bridge_screenshot
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FlutterBridgeScreenshotTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_form_filling.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_form_filling.cfg
new file mode 100644
index 0000000..3f02433
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_form_filling.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan Form Filling UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.form_filling_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FormFillingTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_go_back.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_go_back.cfg
new file mode 100644
index 0000000..1d184e5
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_go_back.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan 'go back' UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.go_back_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=GoBackTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_home_scrolling.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_home_scrolling.cfg
new file mode 100644
index 0000000..be99d4a
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_home_scrolling.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan Home Scrolling Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.home_scrolling_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HomeScrollingTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_idle.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_idle.cfg
new file mode 100644
index 0000000..bd7bb24
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_idle.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan Idle UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.idle_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=IdleTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_knowledge.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_knowledge.cfg
new file mode 100644
index 0000000..f77d4d4
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_knowledge.cfg
@@ -0,0 +1,18 @@
+# Config file for Knowledge Card Tests
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.knowledge_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=KnowledgeTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_knowledge_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_knowledge_master_latest.cfg
new file mode 100644
index 0000000..9d53159
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_knowledge_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Knowledge Card Tests
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.knowledge_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=KnowledgeTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_knowledge_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_knowledge_tsmc.cfg
new file mode 100644
index 0000000..f77d4d4
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_knowledge_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Knowledge Card Tests
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.knowledge_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=KnowledgeTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_music_playback.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_music_playback.cfg
new file mode 100644
index 0000000..eae5bf9
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_music_playback.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan Music Playback UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.music_playback_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MusicPlaybackTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_navigation.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_navigation.cfg
new file mode 100644
index 0000000..29f4aff
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_navigation.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan Navigation UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.navigation_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NavigationTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_ambient.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_ambient.cfg
new file mode 100644
index 0000000..e9bf790
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_ambient.cfg
@@ -0,0 +1,19 @@
+# Config file for Manhattan Perception Ambient UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.ambient_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AmbientTestSequence
+
+# GCS bucket and path for the perception video
+GCS_VIDEO_PATH=perception/test_videos/motion/03ft_3p_bd_2_edit.mp4
+GCS_BUCKET=catatester_test_deps
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_animations.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_animations.cfg
new file mode 100644
index 0000000..678c5f8
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_animations.cfg
@@ -0,0 +1,19 @@
+# Config file for Manhattan Perception Animations UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.animations_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AnimationsTestSequence
+
+# GCS bucket and path for the perception video
+GCS_VIDEO_PATH=perception/test_videos/motion/03ft_3p_bd_2_edit.mp4
+GCS_BUCKET=catatester_test_deps
\ No newline at end of file
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_carousel_load.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_carousel_load.cfg
new file mode 100644
index 0000000..225f0bd
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_carousel_load.cfg
@@ -0,0 +1,19 @@
+# Config file for Manhattan Perception Carousel UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.carousel_loading_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CarouselLoadingTestSequence
+
+# GCS bucket and path for the perception video
+GCS_VIDEO_PATH=perception/test_videos/motion/03ft_3p_bd_2_edit.mp4
+GCS_BUCKET=catatester_test_deps
\ No newline at end of file
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_carousel_scroll.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_carousel_scroll.cfg
new file mode 100644
index 0000000..d284963
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_carousel_scroll.cfg
@@ -0,0 +1,19 @@
+# Config file for Manhattan Perception Carousel UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.carousel_scrolling_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CarouselScrollingTestSequence
+
+# GCS bucket and path for the perception video
+GCS_VIDEO_PATH=perception/test_videos/motion/03ft_3p_bd_2_edit.mp4
+GCS_BUCKET=catatester_test_deps
\ No newline at end of file
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_form_filling.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_form_filling.cfg
new file mode 100644
index 0000000..03074a6
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_form_filling.cfg
@@ -0,0 +1,19 @@
+# Config file for Manhattan Perception Form Filling UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.form_filling_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FormFillingTestSequence
+
+# GCS bucket and path for the perception video
+GCS_VIDEO_PATH=perception/test_videos/motion/03ft_3p_bd_2_edit.mp4
+GCS_BUCKET=catatester_test_deps
\ No newline at end of file
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_go_back.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_go_back.cfg
new file mode 100644
index 0000000..49ef6d7
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_go_back.cfg
@@ -0,0 +1,19 @@
+# Config file for Manhattan Perception 'go back' UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.go_back_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=GoBackTestSequence
+
+# GCS bucket and path for the perception video
+GCS_VIDEO_PATH=perception/test_videos/motion/03ft_3p_bd_2_edit.mp4
+GCS_BUCKET=catatester_test_deps
\ No newline at end of file
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_home_scrolling.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_home_scrolling.cfg
new file mode 100644
index 0000000..e948b75
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_home_scrolling.cfg
@@ -0,0 +1,19 @@
+# Config file for Manhattan Perception Home Scrolling Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.home_scrolling_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HomeScrollingTestSequence
+
+# GCS bucket and path for the perception video
+GCS_VIDEO_PATH=perception/test_videos/motion/03ft_3p_bd_2_edit.mp4
+GCS_BUCKET=catatester_test_deps
\ No newline at end of file
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_music_playback.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_music_playback.cfg
new file mode 100644
index 0000000..61d19af
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_music_playback.cfg
@@ -0,0 +1,19 @@
+# Config file for Manhattan Perception Music Playback UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.music_playback_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MusicPlaybackTestSequence
+
+# GCS bucket and path for the perception video
+GCS_VIDEO_PATH=perception/test_videos/motion/03ft_3p_bd_2_edit.mp4
+GCS_BUCKET=catatester_test_deps
\ No newline at end of file
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_navigation.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_navigation.cfg
new file mode 100644
index 0000000..0b3b156
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_navigation.cfg
@@ -0,0 +1,19 @@
+# Config file for Manhattan Perception Navigation UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.navigation_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NavigationTestSequence
+
+# GCS bucket and path for the perception video
+GCS_VIDEO_PATH=perception/test_videos/motion/03ft_3p_bd_2_edit.mp4
+GCS_BUCKET=catatester_test_deps
\ No newline at end of file
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_recipes.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_recipes.cfg
new file mode 100644
index 0000000..f2db1f0
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_recipes.cfg
@@ -0,0 +1,19 @@
+# Config file for Manhattan Perception Recipes UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.recipes_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=RecipesTestSequence
+
+# GCS bucket and path for the perception video
+GCS_VIDEO_PATH=perception/test_videos/motion/03ft_3p_bd_2_edit.mp4
+GCS_BUCKET=catatester_test_deps
\ No newline at end of file
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_youtube.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_youtube.cfg
new file mode 100644
index 0000000..29692e1
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_perception_youtube.cfg
@@ -0,0 +1,19 @@
+# Config file for Manhattan Perception YouTube Playback UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.youtube_playback_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=YouTubePlaybackTestSequence
+
+# GCS bucket and path for the perception video
+GCS_VIDEO_PATH=perception/test_videos/motion/03ft_3p_bd_2_edit.mp4
+GCS_BUCKET=catatester_test_deps
\ No newline at end of file
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_recipes.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_recipes.cfg
new file mode 100644
index 0000000..6c26489
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_recipes.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan Recipes UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.recipes_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=RecipesTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_system_gestures.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_system_gestures.cfg
new file mode 100644
index 0000000..45ac8ca
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_system_gestures.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan System Gestures UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.system_gestures_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SystemGesturesTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_weather.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_weather.cfg
new file mode 100644
index 0000000..8206556
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_weather.cfg
@@ -0,0 +1,18 @@
+# Config file for Weather Tests
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.weather_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=WeatherTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_weather_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_weather_master_latest.cfg
new file mode 100644
index 0000000..28caf02
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_weather_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Weather Tests
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.weather_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=WeatherTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_weather_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_weather_tsmc.cfg
new file mode 100644
index 0000000..8206556
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_weather_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Weather Tests
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.weather_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=WeatherTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/manhattan_youtube.cfg b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_youtube.cfg
new file mode 100644
index 0000000..c9190db
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/manhattan_youtube.cfg
@@ -0,0 +1,15 @@
+# Config file for Manhattan YouTube Playback UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.manhattan_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ManhattanUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.youtube_playback_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=YouTubePlaybackTestSequence
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_ambient.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_ambient.cfg
new file mode 100644
index 0000000..192d15d
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_ambient.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Ambient UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.ambient_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AmbientTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_animations.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_animations.cfg
new file mode 100644
index 0000000..6a8d75d
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_animations.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Animations UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.animations_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AnimationsTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_animations_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_animations_master_latest.cfg
new file mode 100644
index 0000000..4838365
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_animations_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Manhattan Animations UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.animations_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AnimationsTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_animations_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_animations_tsmc.cfg
new file mode 100644
index 0000000..6a8d75d
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_animations_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Animations UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.animations_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AnimationsTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_back_gestures.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_back_gestures.cfg
new file mode 100644
index 0000000..164a68f
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_back_gestures.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Ambient UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.back_gestures_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=BackGesturesTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_back_gestures_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_back_gestures_master_latest.cfg
new file mode 100644
index 0000000..a8ff8e2
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_back_gestures_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Manhattan Ambient UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.opal_back_gestures_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=BackGesturesTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_back_gestures_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_back_gestures_tsmc.cfg
new file mode 100644
index 0000000..164a68f
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_back_gestures_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Ambient UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.back_gestures_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=BackGesturesTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_load.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_load.cfg
new file mode 100644
index 0000000..57bd0e0
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_load.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Carousel UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.carousel_loading_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CarouselLoadingTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_load_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_load_master_latest.cfg
new file mode 100644
index 0000000..489fbb9
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_load_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Manhattan Carousel UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.carousel_loading_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CarouselLoadingTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_load_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_load_tsmc.cfg
new file mode 100644
index 0000000..57bd0e0
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_load_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Carousel UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.carousel_loading_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CarouselLoadingTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_scroll.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_scroll.cfg
new file mode 100644
index 0000000..1729edc
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_scroll.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Carousel UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.carousel_scrolling_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CarouselScrollingTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_scroll_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_scroll_master_latest.cfg
new file mode 100644
index 0000000..db28a3f
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_scroll_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Manhattan Carousel UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.carousel_scrolling_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CarouselScrollingTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_scroll_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_scroll_tsmc.cfg
new file mode 100644
index 0000000..1729edc
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_carousel_scroll_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Carousel UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.carousel_scrolling_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CarouselScrollingTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_coin_tray_gestures.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_coin_tray_gestures.cfg
new file mode 100644
index 0000000..9cf439d
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_coin_tray_gestures.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan System Gestures UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.coin_tray_gestures_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CoinTrayGesturesTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_cujs.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_cujs.cfg
new file mode 100644
index 0000000..7dfac86
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_cujs.cfg
@@ -0,0 +1,18 @@
+# Config file for Opal CUJ Tests
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.opal_cuj_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OpalCujTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_cujs_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_cujs_master_latest.cfg
new file mode 100644
index 0000000..30275f0
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_cujs_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Opal CUJ Tests
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.opal_cuj_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OpalCujTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_cujs_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_cujs_tsmc.cfg
new file mode 100644
index 0000000..7dfac86
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_cujs_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Opal CUJ Tests
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.opal_cuj_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OpalCujTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_form_filling.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_form_filling.cfg
new file mode 100644
index 0000000..6dc12d8
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_form_filling.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Form Filling UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.form_filling_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FormFillingTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_form_filling_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_form_filling_master_latest.cfg
new file mode 100644
index 0000000..6a72ad9
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_form_filling_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Manhattan Form Filling UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.form_filling_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FormFillingTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_form_filling_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_form_filling_tsmc.cfg
new file mode 100644
index 0000000..6dc12d8
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_form_filling_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Form Filling UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.form_filling_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FormFillingTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_go_back.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_go_back.cfg
new file mode 100644
index 0000000..73779b4
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_go_back.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan 'go back' UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.go_back_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=GoBackTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_go_back_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_go_back_master_latest.cfg
new file mode 100644
index 0000000..bea9a89
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_go_back_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Manhattan 'go back' UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.go_back_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=GoBackTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_go_back_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_go_back_tsmc.cfg
new file mode 100644
index 0000000..73779b4
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_go_back_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan 'go back' UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.go_back_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=GoBackTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_home_scrolling.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_home_scrolling.cfg
new file mode 100644
index 0000000..61d4112
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_home_scrolling.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Home Scrolling Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.home_scrolling_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=HomeScrollingTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_idle.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_idle.cfg
new file mode 100644
index 0000000..f7ea411
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_idle.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Idle UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.idle_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=IdleTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_idle_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_idle_master_latest.cfg
new file mode 100644
index 0000000..d9d6fc0
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_idle_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Manhattan Idle UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.idle_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=IdleTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_idle_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_idle_tsmc.cfg
new file mode 100644
index 0000000..f7ea411
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_idle_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Idle UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.idle_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=IdleTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_music_playback.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_music_playback.cfg
new file mode 100644
index 0000000..2adc794
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_music_playback.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Navigation UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.music_playback_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MusicPlaybackTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_music_playback_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_music_playback_master_latest.cfg
new file mode 100644
index 0000000..16fdebd
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_music_playback_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Manhattan Navigation UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.music_playback_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MusicPlaybackTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_music_playback_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_music_playback_tsmc.cfg
new file mode 100644
index 0000000..2adc794
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_music_playback_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Navigation UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.music_playback_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MusicPlaybackTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_navigation.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_navigation.cfg
new file mode 100644
index 0000000..ed81c0f
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_navigation.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Navigation UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.navigation_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NavigationTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_navigation_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_navigation_master_latest.cfg
new file mode 100644
index 0000000..ca25841
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_navigation_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Manhattan Navigation UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.navigation_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NavigationTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_navigation_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_navigation_tsmc.cfg
new file mode 100644
index 0000000..ed81c0f
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_navigation_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Navigation UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.navigation_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NavigationTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_recipes.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_recipes.cfg
new file mode 100644
index 0000000..d6a257e
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_recipes.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Recipes UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.recipes_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=RecipesTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_recipes_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_recipes_master_latest.cfg
new file mode 100644
index 0000000..d023c13
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_recipes_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Manhattan Recipes UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.recipes_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=RecipesTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_recipes_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_recipes_tsmc.cfg
new file mode 100644
index 0000000..d6a257e
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_recipes_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Recipes UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.recipes_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=RecipesTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_shader_collection.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_shader_collection.cfg
new file mode 100644
index 0000000..9fdc3c5
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_shader_collection.cfg
@@ -0,0 +1,18 @@
+# Config file for Opal shader collection
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.shader_collection_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=ShaderCollectionTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_shader_collection_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_shader_collection_master_latest.cfg
new file mode 100644
index 0000000..70a940e
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_shader_collection_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Opal shader collection
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.shader_collection_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=ShaderCollectionTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_stargazer.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_stargazer.cfg
new file mode 100644
index 0000000..216a30b
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_stargazer.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Stargazer Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.stargazer_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=StargazerTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_stargazer_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_stargazer_master_latest.cfg
new file mode 100644
index 0000000..a9090ba
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_stargazer_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Manhattan Stargazer Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.stargazer_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=StargazerTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_stargazer_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_stargazer_tsmc.cfg
new file mode 100644
index 0000000..216a30b
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_stargazer_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan Stargazer Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.stargazer_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=StargazerTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_system_gestures.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_system_gestures.cfg
new file mode 100644
index 0000000..b1a3508
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_system_gestures.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan System Gestures UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.system_gestures_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SystemGesturesTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_system_gestures_master_latest.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_system_gestures_master_latest.cfg
new file mode 100644
index 0000000..f1ef205
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_system_gestures_master_latest.cfg
@@ -0,0 +1,21 @@
+# Config file for Manhattan System Gestures UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.system_gestures_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SystemGesturesTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
+
+# Override the prebuilt image of Opal
+OPAL_REF=master_latest
diff --git a/lab_system/master/config/tests/manhattan_ui_perf/opal_system_gestures_tsmc.cfg b/lab_system/master/config/tests/manhattan_ui_perf/opal_system_gestures_tsmc.cfg
new file mode 100644
index 0000000..b1a3508
--- /dev/null
+++ b/lab_system/master/config/tests/manhattan_ui_perf/opal_system_gestures_tsmc.cfg
@@ -0,0 +1,18 @@
+# Config file for Manhattan System Gestures UI Performance Test Suite
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.manhattan.perf.opal_ui_perf_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OpalUiPerfTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.manhattan.perf.system_gestures_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SystemGesturesTestSequence
+
+# Indicates whether or not we should sign the device in
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/ml_framework/ml_framework_power_test.cfg b/lab_system/master/config/tests/ml_framework/ml_framework_power_test.cfg
new file mode 100644
index 0000000..6065687
--- /dev/null
+++ b/lab_system/master/config/tests/ml_framework/ml_framework_power_test.cfg
@@ -0,0 +1,17 @@
+# Configuration values for ML Framework Power test.
+#
+# PROPERTIES
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PowerMlFramework
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ml_framework.ml_framework_power_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NdmTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=experimental.ndm.ndm_test_sequence
+
+# Seconds to sleep between inferences
+TEST_SLEEP_TIME=15
diff --git a/lab_system/master/config/tests/ml_framework/ml_framework_sysmon_test.cfg b/lab_system/master/config/tests/ml_framework/ml_framework_sysmon_test.cfg
new file mode 100644
index 0000000..bc92a8f
--- /dev/null
+++ b/lab_system/master/config/tests/ml_framework/ml_framework_sysmon_test.cfg
@@ -0,0 +1,14 @@
+# Configuration values for MojoSpy Sysmon test.
+#
+# PROPERTIES
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MlFrameworkSysmonTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ml_framework.ml_framework_sysmon_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NdmTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=experimental.ndm.ndm_test_sequence
diff --git a/lab_system/master/config/tests/mojo_spy_tests/mojo_spy_sysmom_test.cfg b/lab_system/master/config/tests/mojo_spy_tests/mojo_spy_sysmom_test.cfg
new file mode 100644
index 0000000..926a450
--- /dev/null
+++ b/lab_system/master/config/tests/mojo_spy_tests/mojo_spy_sysmom_test.cfg
@@ -0,0 +1,18 @@
+# Configuration values for MojoSpy Sysmon test.
+#
+# PROPERTIES
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SysmonMlFramework
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mojo_spy_tests.sysmon_mojo_spy_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NdmTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=experimental.ndm.ndm_test_sequence
+
+# Points to the folder containing JSON and raw images located in
+# gs://cast-test-data gcs bucket.
+RAW_DATA_PATH=mojospy/sysmon_test/default/
diff --git a/lab_system/master/config/tests/monitoring/playback.cfg b/lab_system/master/config/tests/monitoring/playback.cfg
new file mode 100644
index 0000000..93860e3
--- /dev/null
+++ b/lab_system/master/config/tests/monitoring/playback.cfg
@@ -0,0 +1,15 @@
+# Config file for video playback profiler test
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.monitoring.playback_profiler_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PlaybackProfilerTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
diff --git a/lab_system/master/config/tests/monitoring/playback/test_defs.json b/lab_system/master/config/tests/monitoring/playback/test_defs.json
new file mode 100644
index 0000000..dcdb51e
--- /dev/null
+++ b/lab_system/master/config/tests/monitoring/playback/test_defs.json
@@ -0,0 +1,44 @@
+[
+  {
+    "group_name": "monitoring",
+    "slave_name": "playback_profiler",
+    "test_name": "playback",
+    "identifier": "playback_profiler",
+    "device_type": "eureka",
+    "device_group": "eureka_debug",
+    "hidden": "true",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "playback_profiler_v2",
+    "test_name": "playback",
+    "identifier": "playback_profiler_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "hidden": "true",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "playback_profiler",
+    "test_name": "playback",
+    "identifier": "playback_profiler_prod",
+    "device_type": "eureka",
+    "device_group": "eureka_debug",
+    "branch": "on_demand",
+    "hidden": "true",
+    "scheduler": "0 20 * * *"
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "playback_profiler_v2",
+    "test_name": "playback",
+    "identifier": "playback_profiler_prod_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "branch": "on_demand",
+    "hidden": "true",
+    "scheduler": "0 20 * * *"
+  }
+]
diff --git a/lab_system/master/config/tests/monitoring/youtube/test_defs.json b/lab_system/master/config/tests/monitoring/youtube/test_defs.json
new file mode 100644
index 0000000..aa584c3
--- /dev/null
+++ b/lab_system/master/config/tests/monitoring/youtube/test_defs.json
@@ -0,0 +1,154 @@
+[
+  {
+    "group_name": "monitoring",
+    "slave_name": "youtube_profiler",
+    "test_name": "youtube_live_only",
+    "identifier": "youtube_live_only",
+    "device_type": "eureka",
+    "device_group": "eureka_debug",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "scheduler": "0 0,12 * * *"
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "youtube_profiler_v2",
+    "test_name": "youtube_live_only",
+    "identifier": "youtube_live_only_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "scheduler": "0 0,12 * * *"
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "youtube_profiler_v3",
+    "test_name": "youtube_live_only",
+    "identifier": "youtube_live_only_v3",
+    "device_type": "earth",
+    "device_group": "earth_eth",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "scheduler": "0 0,12 * * *"
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "youtube_profiler",
+    "test_name": "youtube_live_dvr",
+    "identifier": "youtube_live_dvr",
+    "device_type": "eureka",
+    "device_group": "eureka_debug",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "scheduler": "0 4,16 * * *"
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "youtube_profiler_v2",
+    "test_name": "youtube_live_dvr",
+    "identifier": "youtube_live_dvr_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "scheduler": "0 4,16 * * *"
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "youtube_profiler_v3",
+    "test_name": "youtube_live_dvr",
+    "identifier": "youtube_live_dvr_v3",
+    "device_type": "earth",
+    "device_group": "earth_eth",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "scheduler": "0 4,16 * * *"
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "youtube_profiler",
+    "test_name": "youtube_live_hfr_dvr",
+    "identifier": "youtube_live_hfr_dvr",
+    "device_type": "eureka",
+    "device_group": "eureka_debug",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "scheduler": "0 8,20 * * *"
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "youtube_profiler_v2",
+    "test_name": "youtube_live_hfr_dvr",
+    "identifier": "youtube_live_hfr_dvr_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "scheduler": "0 8,20 * * *"
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "youtube_profiler_v3",
+    "test_name": "youtube_live_hfr_dvr",
+    "identifier": "youtube_live_hfr_dvr_v3",
+    "device_type": "earth",
+    "device_group": "earth_eth",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "scheduler": "0 8,20 * * *"
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "youtube_profiler_v2",
+    "test_name": "youtube",
+    "identifier": "youtube_profiler_prod_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "hidden": "true",
+    "branch": "on_demand"
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "youtube_profiler",
+    "test_name": "playback",
+    "identifier": "youtube_playback",
+    "device_type": "eureka",
+    "device_group": "eureka_debug",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "scheduler": "0 0,12 * * *"
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "youtube_profiler_v2",
+    "test_name": "playback",
+    "identifier": "youtube_playback_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "scheduler": "0 0,12 * * *"
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "youtube_profiler_v3",
+    "test_name": "playback",
+    "identifier": "youtube_playback_v3",
+    "device_type": "earth",
+    "device_group": "earth_eth",
+    "hidden": "true",
+    "branch": ["master", "release"],
+    "scheduler": "0 0,12 * * *"
+  },
+  {
+    "group_name": "monitoring",
+    "slave_name": "youtube_profiler_v2",
+    "test_name": "playback",
+    "identifier": "youtube_playback_prod_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "hidden": "true",
+    "branch": "on_demand"
+  }
+]
diff --git a/lab_system/master/config/tests/monitoring/youtube_live_dvr.cfg b/lab_system/master/config/tests/monitoring/youtube_live_dvr.cfg
new file mode 100644
index 0000000..f0ce7a0
--- /dev/null
+++ b/lab_system/master/config/tests/monitoring/youtube_live_dvr.cfg
@@ -0,0 +1,15 @@
+# Config file for YouTube profiler test with DVR live stream
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.monitoring.youtube_profiler_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=YoutubeLiveDvrProfilerTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
diff --git a/lab_system/master/config/tests/monitoring/youtube_live_hfr_dvr.cfg b/lab_system/master/config/tests/monitoring/youtube_live_hfr_dvr.cfg
new file mode 100644
index 0000000..c0e7299
--- /dev/null
+++ b/lab_system/master/config/tests/monitoring/youtube_live_hfr_dvr.cfg
@@ -0,0 +1,15 @@
+# Config file for YouTube profiler test with Live HFR DVR stream
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.monitoring.youtube_profiler_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=YoutubeLiveHfrDvrProfilerTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
diff --git a/lab_system/master/config/tests/monitoring/youtube_live_only.cfg b/lab_system/master/config/tests/monitoring/youtube_live_only.cfg
new file mode 100644
index 0000000..27122d7
--- /dev/null
+++ b/lab_system/master/config/tests/monitoring/youtube_live_only.cfg
@@ -0,0 +1,15 @@
+# Config file for YouTube profiler test for live only stream
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.monitoring.youtube_profiler_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=YoutubeLiveOnlyProfilerTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
diff --git a/lab_system/master/config/tests/mpl/mpl_dogfood_4k.cfg b/lab_system/master/config/tests/mpl/mpl_dogfood_4k.cfg
new file mode 100644
index 0000000..d6950f0
--- /dev/null
+++ b/lab_system/master/config/tests/mpl/mpl_dogfood_4k.cfg
@@ -0,0 +1,18 @@
+# Config file for 4K mple2e tests using dogfood version of mpl.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mpl.mpl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MplPlayToEndDogfood4KTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.mpl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MplTestSequence
+
+# Use SSH tunnel when connecting to DUT.
+
diff --git a/lab_system/master/config/tests/mpl/mpl_dogfood_dv.cfg b/lab_system/master/config/tests/mpl/mpl_dogfood_dv.cfg
new file mode 100644
index 0000000..1dde1f1
--- /dev/null
+++ b/lab_system/master/config/tests/mpl/mpl_dogfood_dv.cfg
@@ -0,0 +1,16 @@
+# Config file for Dolby Vision Tests using dogfood version of MPL.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mpl.mpl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MplBasicDogfoodDVTestCase,MplGetQualityLevelDogfoodDVTestCase,MplLoadDogfoodDVTestCase,MplLoadWithInitialTimeDogfoodDVTestCase,MplPlayToEndDogfoodDVTestCase,MplPreloadDogfoodDVTestCase,MplSeekDogfoodDVTestCase,MplSkipRequestDogfoodDVTestCase,MplSkipRequestFailDogfoodDVTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.mpl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MplTestSequence
+
diff --git a/lab_system/master/config/tests/mpl/mpl_dogfood_lg.cfg b/lab_system/master/config/tests/mpl/mpl_dogfood_lg.cfg
new file mode 100644
index 0000000..bfeb199
--- /dev/null
+++ b/lab_system/master/config/tests/mpl/mpl_dogfood_lg.cfg
@@ -0,0 +1,15 @@
+# Config file for Dolby Vision Tests using dogfood version of MPL.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mpl.mpl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MplBasicDogfoodLGTestCase,MplGetQualityLevelDogfoodLGTestCase,MplLoadDogfoodLGTestCase,MplLoadWithInitialTimeDogfoodLGTestCase,MplPlayToEndDogfoodLGTestCase,MplPreloadDogfoodLGTestCase,MplSeekDogfoodLGTestCase,MplSkipRequestDogfoodLGTestCase,MplSkipRequestFailDogfoodLGTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.mpl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MplTestSequence
diff --git a/lab_system/master/config/tests/mpl/mpl_release.cfg b/lab_system/master/config/tests/mpl/mpl_release.cfg
new file mode 100644
index 0000000..3499516
--- /dev/null
+++ b/lab_system/master/config/tests/mpl/mpl_release.cfg
@@ -0,0 +1,18 @@
+# Config file for mple2e tests using the release version of mpl.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mpl.mpl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MplBasicReleaseTestCase,MplEnableCaptionsReleaseTestCase,MplGetQualityLevelReleaseTestCase,MplInstantPlayReleaseTestCase,MplLoadReleaseTestCase,MplLoadWithInitialTimeReleaseTestCase,MplPlayToEndReleaseTestCase,MplPreloadReleaseTestCase,MplProcessMetadataReleaseTestCase,MplReloadReleaseTestCase,MplSeekReleaseTestCase,MplSkipRequestReleaseTestCase,MplSkipRequestFailReleaseTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.mpl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MplTestSequence
+
+# Use SSH tunnel when connecting to DUT.
+
diff --git a/lab_system/master/config/tests/mpl/mpl_release_4k.cfg b/lab_system/master/config/tests/mpl/mpl_release_4k.cfg
new file mode 100644
index 0000000..bba4e67
--- /dev/null
+++ b/lab_system/master/config/tests/mpl/mpl_release_4k.cfg
@@ -0,0 +1,18 @@
+# Config file for 4K mple2e tests using the release version of mpl.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mpl.mpl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MplPlayToEndRelease4KTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.mpl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MplTestSequence
+
+# Use SSH tunnel when connecting to DUT.
+
diff --git a/lab_system/master/config/tests/mpl/mpl_release_dv.cfg b/lab_system/master/config/tests/mpl/mpl_release_dv.cfg
new file mode 100644
index 0000000..d5709b0
--- /dev/null
+++ b/lab_system/master/config/tests/mpl/mpl_release_dv.cfg
@@ -0,0 +1,15 @@
+# Config file for Dolby Vision tests using the release version of MPL.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mpl.mpl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MplBasicReleaseDVTestCase,MplGetQualityLevelReleaseDVTestCase,MplLoadReleaseDVTestCase,MplLoadWithInitialTimeReleaseDVTestCase,MplPlayToEndReleaseDVTestCase,MplPreloadReleaseDVTestCase,MplSeekReleaseDVTestCase,MplSkipRequestReleaseDVTestCase,MplSkipRequestFailReleaseDVTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.mpl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MplTestSequence
diff --git a/lab_system/master/config/tests/mpl/mpl_release_lg.cfg b/lab_system/master/config/tests/mpl/mpl_release_lg.cfg
new file mode 100644
index 0000000..3f1e1ea
--- /dev/null
+++ b/lab_system/master/config/tests/mpl/mpl_release_lg.cfg
@@ -0,0 +1,15 @@
+# Config file for Dolby Vision tests using the release version of MPL.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mpl.mpl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MplBasicReleaseLGTestCase,MplGetQualityLevelReleaseLGTestCase,MplLoadReleaseLGTestCase,MplLoadWithInitialTimeReleaseLGTestCase,MplPlayToEndReleaseLGTestCase,MplPreloadReleaseLGTestCase,MplSeekReleaseLGTestCase,MplSkipRequestReleaseLGTestCase,MplSkipRequestFailReleaseLGTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.mpl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MplTestSequence
diff --git a/lab_system/master/config/tests/mpl/mpl_staging_4k.cfg b/lab_system/master/config/tests/mpl/mpl_staging_4k.cfg
new file mode 100644
index 0000000..6801d53
--- /dev/null
+++ b/lab_system/master/config/tests/mpl/mpl_staging_4k.cfg
@@ -0,0 +1,18 @@
+# Config file 4K mple2e tests using development version of mpl.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mpl.mpl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MplPlayToEndDevelopment4KTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.mpl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MplTestSequence
+
+# Use SSH tunnel when connecting to DUT
+
diff --git a/lab_system/master/config/tests/mpl/mpl_staging_dv.cfg b/lab_system/master/config/tests/mpl/mpl_staging_dv.cfg
new file mode 100644
index 0000000..2e31b72
--- /dev/null
+++ b/lab_system/master/config/tests/mpl/mpl_staging_dv.cfg
@@ -0,0 +1,16 @@
+# Config for Dolby Vision tests using development version of MPL.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mpl.mpl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MplBasicDevelopmentDVTestCase,MplGetQualityLevelDevelopmentDVTestCase,MplLoadDevelopmentDVTestCase,MplLoadWithInitialTimeDevelopmentDVTestCase,MplPlayToEndDevelopmentDVTestCase,MplPreloadDevelopmentDVTestCase,MplSeekDevelopmentDVTestCase,MplSkipRequestDevelopmentDVTestCase,MplSkipRequestFailDevelopmentDVTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.mpl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MplTestSequence
+
diff --git a/lab_system/master/config/tests/mpl/mpl_staging_lg.cfg b/lab_system/master/config/tests/mpl/mpl_staging_lg.cfg
new file mode 100644
index 0000000..c75a390
--- /dev/null
+++ b/lab_system/master/config/tests/mpl/mpl_staging_lg.cfg
@@ -0,0 +1,15 @@
+# Config for Dolby Vision tests using development version of MPL.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mpl.mpl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MplBasicDevelopmentLGTestCase,MplGetQualityLevelDevelopmentLGTestCase,MplLoadDevelopmentLGTestCase,MplLoadWithInitialTimeDevelopmentLGTestCase,MplPlayToEndDevelopmentLGTestCase,MplPreloadDevelopmentLGTestCase,MplSeekDevelopmentLGTestCase,MplSkipRequestDevelopmentLGTestCase,MplSkipRequestFailDevelopmentLGTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.mpl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MplTestSequence
diff --git a/lab_system/master/config/tests/mpl/test_defs.json b/lab_system/master/config/tests/mpl/test_defs.json
new file mode 100644
index 0000000..3d71166
--- /dev/null
+++ b/lab_system/master/config/tests/mpl/test_defs.json
@@ -0,0 +1,140 @@
+[
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_staging_4k",
+    "identifier": "mpl_staging_4k_steak",
+    "device_type": "luther_b2",
+    "device_group": "av_drm",
+    "build_flavor": "user",
+    "branch": ["master", "next_release", "release"]
+  },
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_staging_lg",
+    "identifier": "mpl_staging_lg_steak",
+    "device_type": "luther_b2",
+    "device_group": "luther_lg",
+    "branch": ["release", "next_release", "master"]
+  },
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_staging_dv",
+    "identifier": "mpl_staging_dv_steak",
+    "device_type": "luther_b2",
+    "device_group": "luther_vizio",
+    "branch": ["release", "next_release", "master"]
+  },
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_dogfood_4k",
+    "identifier": "mpl_dogfood_4k_steak",
+    "device_type": "luther_b2",
+    "device_group": "av_drm",
+    "build_flavor": "user",
+    "branch": ["master", "next_release", "release"]
+  },
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_dogfood_lg",
+    "identifier": "mpl_dogfood_lg_steak",
+    "device_type": "luther_b2",
+    "device_group": "luther_lg",
+    "branch": ["release", "next_release", "master"]
+  },
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_dogfood_dv",
+    "identifier": "mpl_dogfood_dv_steak",
+    "device_type": "luther_b2",
+    "device_group": "luther_vizio",
+    "branch": ["release", "next_release", "master"]
+  },
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_release_4k",
+    "identifier": "mpl_release_4k_steak",
+    "device_type": "luther_b2",
+    "device_group": "av_drm",
+    "build_flavor": "user",
+    "branch": ["master", "next_release", "release"]
+  },
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_release_lg",
+    "identifier": "mpl_release_lg_steak",
+    "device_type": "luther_b2",
+    "device_group": "luther_lg",
+    "branch": ["release", "next_release", "master"]
+  },
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_release_dv",
+    "identifier": "mpl_release_dv_steak",
+    "device_type": "luther_b2",
+    "device_group": "luther_vizio",
+    "branch": ["release", "next_release", "master"]
+  },
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_release",
+    "identifier": "mpl_release_bubbletea",
+    "device_type": "bubbletea",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  },
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_release",
+    "identifier": "mpl_release_guabao",
+    "device_type": "guabao",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  },
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_release",
+    "identifier": "mpl_release_mars_us",
+    "device_type": "Mars_US",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_mediatek_master", "cast_tv_mediatek_release", "cast_tv_mediatek_next_release"]
+  },
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_release",
+    "identifier": "mpl_release_N00007",
+    "device_type": "N00007",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_novatek_master", "cast_tv_novatek_release", "cast_tv_novatek_next_release"]
+  },
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_release",
+    "identifier": "mpl_release_suncake",
+    "device_type": "suncake",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  },
+  {
+    "group_name": "mpl",
+    "slave_name": "javascript_no_security",
+    "test_name": "mpl_release",
+    "identifier": "mpl_release_taroball",
+    "device_type": "taroball",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  }
+]
diff --git a/lab_system/master/config/tests/mpl_dogfood/mpl_dogfood.cfg b/lab_system/master/config/tests/mpl_dogfood/mpl_dogfood.cfg
new file mode 100644
index 0000000..2a9801f
--- /dev/null
+++ b/lab_system/master/config/tests/mpl_dogfood/mpl_dogfood.cfg
@@ -0,0 +1,18 @@
+# Config file for mple2e using dogfood version of mpl.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mpl.mpl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MplBasicDogfoodTestCase,MplEnableCaptionsDogfoodTestCase,MplGetQualityLevelDogfoodTestCase,MplInstantPlayDogfoodTestCase,MplLoadDogfoodTestCase,MplLoadWithInitialTimeDogfoodTestCase,MplPlayToEndDogfoodTestCase,MplPreloadDogfoodTestCase,MplProcessMetadataDogfoodTestCase,MplReloadDogfoodTestCase,MplSeekDogfoodTestCase,MplSkipRequestDogfoodTestCase,MplSkipRequestFailDogfoodTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.mpl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MplTestSequence
+
+# Use SSH tunnel when connecting to DUT.
+
diff --git a/lab_system/master/config/tests/mpl_staging/mpl_staging.cfg b/lab_system/master/config/tests/mpl_staging/mpl_staging.cfg
new file mode 100644
index 0000000..86e8aaf
--- /dev/null
+++ b/lab_system/master/config/tests/mpl_staging/mpl_staging.cfg
@@ -0,0 +1,18 @@
+# Config file mple2e tests using development version of mpl.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mpl.mpl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MplBasicDevelopmentTestCase,MplEnableCaptionsDevelopmentTestCase,MplGetQualityLevelDevelopmentTestCase,MplInstantPlayDevelopmentTestCase,MplLoadDevelopmentTestCase,MplLoadWithInitialTimeDevelopmentTestCase,MplPlayToEndDevelopmentTestCase,MplPreloadDevelopmentTestCase,MplProcessMetadataDevelopmentTestCase,MplReloadDevelopmentTestCase,MplSeekDevelopmentTestCase,MplSkipRequestDevelopmentTestCase,MplSkipRequestFailDevelopmentTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.mpl_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MplTestSequence
+
+# Use SSH tunnel when connecting to DUT.
+
diff --git a/lab_system/master/config/tests/mse_eme/mse_eme.cfg b/lab_system/master/config/tests/mse_eme/mse_eme.cfg
new file mode 100644
index 0000000..486e3d5
--- /dev/null
+++ b/lab_system/master/config/tests/mse_eme/mse_eme.cfg
@@ -0,0 +1,18 @@
+# Config file for MSE-EME JavaScript test suite.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mse_eme.mse_eme_tip_test_case
+
+# Name of the TestCase sub-class:
+# b/27450353 EmeVP9TestCase has to be reactivated once it is fixed
+TEST_CASE_CLASS=MSEConformanceTestCase,EMEConformanceTestCase,ProgressiveTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.mse_eme.mse_eme_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MseEmeTestSequence
+
+
diff --git a/lab_system/master/config/tests/mse_eme/test_defs.json b/lab_system/master/config/tests/mse_eme/test_defs.json
new file mode 100644
index 0000000..39db4a1
--- /dev/null
+++ b/lab_system/master/config/tests/mse_eme/test_defs.json
@@ -0,0 +1,11 @@
+[
+  {
+    "group_name": "mse_eme",
+    "slave_name": "eurtest_mse_eme_tv",
+    "test_name": "mse_eme",
+    "identifier": "mse_eme_dragonfly",
+    "device_type": "dragonfly",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_dragonfly_master", "cast_tv_dragonfly_release", "cast_tv_dragonfly_next_release"]
+  }
+]
diff --git a/lab_system/master/config/tests/mse_eme_legacy/mse_eme_legacy.cfg b/lab_system/master/config/tests/mse_eme_legacy/mse_eme_legacy.cfg
new file mode 100644
index 0000000..0106b32
--- /dev/null
+++ b/lab_system/master/config/tests/mse_eme_legacy/mse_eme_legacy.cfg
@@ -0,0 +1,17 @@
+# Config file for MSE-EME JavaScript test suite.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.mse_eme.mse_eme_legacy_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MSEConformanceTestCase,EMEConformanceTestCase,ProgressiveTestCase,MSEConformance2016TestCase,EMEConformance2016TestCase,Progressive2016TestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.mse_eme.mse_eme_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MseEmeTestSequence
+
+
diff --git a/lab_system/master/config/tests/mse_eme_legacy/test_defs.json b/lab_system/master/config/tests/mse_eme_legacy/test_defs.json
new file mode 100644
index 0000000..206e8ad
--- /dev/null
+++ b/lab_system/master/config/tests/mse_eme_legacy/test_defs.json
@@ -0,0 +1,47 @@
+[
+  {
+    "group_name": "mse_eme",
+    "slave_name": "eurtest_mse_eme_tv_legacy",
+    "test_name": "mse_eme_legacy",
+    "identifier": "mse_eme_guabao",
+    "device_type": "guabao",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  },
+  {
+    "group_name": "mse_eme",
+    "slave_name": "eurtest_mse_eme_tv_legacy",
+    "test_name": "mse_eme_legacy",
+    "identifier": "mse_eme_mars_us",
+    "device_type": "Mars_US",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_mediatek_master", "cast_tv_mediatek_release", "cast_tv_mediatek_next_release"]
+  },
+  {
+    "group_name": "mse_eme",
+    "slave_name": "eurtest_mse_eme_tv_legacy",
+    "test_name": "mse_eme_legacy",
+    "identifier": "mse_eme_N00007",
+    "device_type": "N00007",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_novatek_master", "cast_tv_novatek_release", "cast_tv_novatek_next_release"]
+  },
+  {
+    "group_name": "mse_eme",
+    "slave_name": "eurtest_mse_eme_tv_legacy",
+    "test_name": "mse_eme_legacy",
+    "identifier": "mse_eme_suncake",
+    "device_type": "suncake",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  },
+  {
+    "group_name": "mse_eme",
+    "slave_name": "eurtest_mse_eme_tv_legacy",
+    "test_name": "mse_eme_legacy",
+    "identifier": "mse_eme_taroball",
+    "device_type": "taroball",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  }
+]
diff --git a/lab_system/master/config/tests/multizone/dli_power_switch.json b/lab_system/master/config/tests/multizone/dli_power_switch.json
new file mode 100644
index 0000000..8d0c726
--- /dev/null
+++ b/lab_system/master/config/tests/multizone/dli_power_switch.json
@@ -0,0 +1,54 @@
+[
+  {
+    "ip": "100.107.130.213",
+    "username": "admin",
+    "password": "password",
+    "receiver_id_to_port_map":
+    {
+      "100.107.130.226": 3,
+      "100.107.130.228": 8,
+      "100.107.130.229": 4,
+      "100.107.130.230": 7
+    }
+  },
+  {
+    "ip": "100.107.130.215",
+    "username": "admin",
+    "password": "password",
+    "receiver_id_to_port_map":
+    {
+      "100.107.130.216": 7,
+      "100.107.130.217": 2,
+      "100.107.130.219": 6,
+      "100.107.130.222": 3
+    }
+  },
+  {
+    "ip": "100.107.130.237",
+    "username": "admin",
+    "password": "chrome4all",
+    "receiver_id_to_port_map":
+    {
+      "100.107.130.204": 6,
+      "100.107.130.205": 8,
+      "100.107.130.218": 3,
+      "100.107.130.220": 4,
+      "100.107.130.221": 7,
+      "100.107.130.231": 1,
+      "100.107.130.233": 2,
+      "100.107.130.234": 5
+    }
+  },
+  {
+    "ip": "100.107.130.214",
+    "username": "admin",
+    "password": "chrome4all",
+    "receiver_id_to_port_map":
+    {
+      "100.107.130.74": 1,
+      "100.107.130.75": 2,
+      "100.107.130.76": 3,
+      "100.107.130.77": 4
+    }
+  }
+]
diff --git a/lab_system/master/config/tests/multizone/hp_procurve_switch_config.json b/lab_system/master/config/tests/multizone/hp_procurve_switch_config.json
new file mode 100644
index 0000000..8ee15bd
--- /dev/null
+++ b/lab_system/master/config/tests/multizone/hp_procurve_switch_config.json
@@ -0,0 +1,27 @@
+[
+  {
+    "host": "100.107.130.211",
+    "username": "admin",
+    "password": "chrome4all",
+    "receiver_id_to_port_map":
+    {
+      "100.107.130.204": "4",
+      "100.107.130.205": "5",
+      "100.107.130.216": "19",
+      "100.107.130.217": "21",
+      "100.107.130.218": "17",
+      "100.107.130.219": "23",
+      "100.107.130.220": "16",
+      "100.107.130.221": "15",
+      "100.107.130.222": "22",
+      "100.107.130.226": "7",
+      "100.107.130.228": "10",
+      "100.107.130.229": "8",
+      "100.107.130.230": "9",
+      "100.107.130.231": "12",
+      "100.107.130.233": "13",
+      "100.107.130.234": "14"
+    },
+    "infrastructure_ports": []
+  }
+]
diff --git a/lab_system/master/config/tests/multizone/multizone.cfg b/lab_system/master/config/tests/multizone/multizone.cfg
new file mode 100644
index 0000000..7e9ce16
--- /dev/null
+++ b/lab_system/master/config/tests/multizone/multizone.cfg
@@ -0,0 +1,97 @@
+# Config file for multizone tests that need equipment other than the
+# receivers (e.g., power or network switches, etc.) but don't need Wi-Fi
+# connectivity.
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.multizone.test_cases
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MultizoneSyncMeasurementTestCase,MultizoneOpusSyncMeasurementTestCase,MultizoneAacSyncMeasurementTestCase,MultizoneCastTerminationTestCase,MultizoneDeviceDiscoveryTestCase,MultizoneGroupingTestCase,MultizoneLatencyCalibrationTestCase,MultizoneMultipleMediaControllersTestCase,MultizoneMultiplePartiesTestCase,MultizoneGroupMuteTestCaseUsingReceiverNamespace,MultizoneSingleDeviceMuteTestCaseUsingMultizoneNamespace,MultizoneSingleDeviceMuteTestCaseUsingReceiverNamespace
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.multizone.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MultizoneTestSequence
+
+# Path to the JSON file (relative to the location of this config file)
+# with the description of the power switch host and its configuration.
+# The remote system is assumed to allow a login by providing username and
+# password.
+#
+# The file has the following format:
+# [
+#   {
+#     "ip": "<IP>",
+#     "username": "<username>",
+#     "password": "<password>",
+#     "receiver_id_to_port_map":
+#     {
+#       "<receiver eth IP address>": "<power switch port>",
+#       ...
+#     }
+#   },
+#   ...
+# ]
+POWER_SWITCH_CONFIG_FILENAME=dli_power_switch.json
+
+# Path to the JSON file (relative to the location of this config file)
+# with the description of the managed network switch and its configuration.
+# The remote system is assumed to allow a login by providing username and
+# password.
+#
+# The file has the following format:
+# [
+#   {
+#     "host": "<hostname or IP address>",
+#     "username": "<username used to login>",
+#     "password": "<password for the user>",
+#     "receiver_id_to_port_map":
+#     {
+#       "<receiver ID>": "<receiver's corresponding ethernet port>",
+#     },
+#     "infrastructure_ports": ["<number of always on port>", ...]
+#   }
+# ]
+NETWORK_SWITCH_CONFIG_FILENAME=hp_procurve_switch_config.json
+
+# Path in the media server to the audio file to play in the group. The audio must consist
+# of pulsed sine waves, like:
+#
+#      _                _               _
+#     / \              / \             / \
+#~~~~~   \   ~~~~~~~~~~   \   ~~~~~~~~~~  \  ~~~~~ ...
+#         \_/              \_/             \/
+#|---------------|     |-----|
+#  fundamental          sine
+#  period               pulse
+#            |--------|
+#             low volume noise
+#             or silence
+TESTFILE_PATH=static/multizone/sine_noise_s48k_p100msecs_d90sec.mp3
+# Duration of the media in seconds.
+TESTFILE_LENGTH_SECS=90
+# Mime type of the media file.
+TESTFILE_MIME_TYPE=audio/mp3
+THREE_MINS_TESTFILE_PATH=static/multizone/noise-sine_500Hz_100msec_44.1kHz_180secs.mp3
+THREE_MINS_TESTFILE_LENGTH_SECS=180
+THREE_MINS_TESTFILE_MIME_TYPE=audio/mp3
+OPUS_TESTFILE_PATH=static/multizone/sine_noise_s48k_p100msecs_d90sec.opus
+OPUS_TESTFILE_MIME_TYPE=audio/ogg
+OPUS_TESTFILE_LENGTH_SECS=90
+AAC_TESTFILE_PATH=static/multizone/sine_noise_s48k_p100msecs_d90sec.mp4
+AAC_TESTFILE_MIME_TYPE=audio/mp4
+AAC_TESTFILE_LENGTH_SECS=90
+
+# Period of the pulses in the media file.
+TESTFILE_FUNDAMENTAL_PERIOD_SEC=0.1
+# Duration of the sine pulse.
+TESTFILE_PULSE_DURATION_SEC=0.002
+# Amplitude below which the receiver is considered to not be playing back.
+TESTFILE_DROPOUT_TRESHOLD=0.3
+# Amplitude for silence value. Adapt to noise level.
+TESTFILE_SILENCE_TRESHOLD=0.05
+# Minimum silence length to be interpreted as dropout in seconds
+MIN_SILENCE_LENGTH_SEC=0.005
+
+
diff --git a/lab_system/master/config/tests/multizone/multizone_sync_test.cfg b/lab_system/master/config/tests/multizone/multizone_sync_test.cfg
new file mode 100644
index 0000000..ae80bf2
--- /dev/null
+++ b/lab_system/master/config/tests/multizone/multizone_sync_test.cfg
@@ -0,0 +1,54 @@
+# Config file for sync tests
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.multizone.test_cases
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MultizoneSyncMeasurementTestCase,MultizoneOpusSyncMeasurementTestCase,MultizoneAacSyncMeasurementTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.multizone.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MultizoneTestSequence
+
+# Path in the media server to the audio file to play in the group. The audio must consist
+# of pulsed sine waves, like:
+#
+#      _                _               _
+#     / \              / \             / \
+#~~~~~   \   ~~~~~~~~~~   \   ~~~~~~~~~~  \  ~~~~~ ...
+#         \_/              \_/             \/
+#|---------------|     |-----|
+#  fundamental          sine
+#  period               pulse
+#            |--------|
+#             low volume noise
+#             or silence
+TESTFILE_PATH=static/multizone/sine_noise_s48k_p100msecs_d90sec.mp3
+# Duration of the media in seconds.
+TESTFILE_LENGTH_SECS=90
+# Mime type of the media file.
+TESTFILE_MIME_TYPE=audio/mp3
+THREE_MINS_TESTFILE_PATH=static/multizone/noise-sine_500Hz_100msec_44.1kHz_180secs.mp3
+THREE_MINS_TESTFILE_LENGTH_SECS=180
+THREE_MINS_TESTFILE_MIME_TYPE=audio/mp3
+OPUS_TESTFILE_PATH=static/multizone/sine_noise_s48k_p100msecs_d90sec.opus
+OPUS_TESTFILE_MIME_TYPE=audio/ogg
+OPUS_TESTFILE_LENGTH_SECS=90
+AAC_TESTFILE_PATH=static/multizone/sine_noise_s48k_p100msecs_d90sec.mp4
+AAC_TESTFILE_MIME_TYPE=audio/mp4
+AAC_TESTFILE_LENGTH_SECS=90
+
+# Period of the pulses in the media file.
+TESTFILE_FUNDAMENTAL_PERIOD_SEC=0.1
+# Duration of the sine pulse.
+TESTFILE_PULSE_DURATION_SEC=0.002
+# Amplitude below which the receiver is considered to not be playing back.
+TESTFILE_DROPOUT_TRESHOLD=0.3
+# Amplitude for silence value. Adapt to noise level.
+TESTFILE_SILENCE_TRESHOLD=0.05
+# Minimum silence length to be interpreted as dropout in seconds
+MIN_SILENCE_LENGTH_SEC=0.005
+
+
diff --git a/lab_system/master/config/tests/multizone/wifi_config.json b/lab_system/master/config/tests/multizone/wifi_config.json
new file mode 100644
index 0000000..707fa93
--- /dev/null
+++ b/lab_system/master/config/tests/multizone/wifi_config.json
@@ -0,0 +1,52 @@
+{
+  "box1":
+  {
+    "networks":
+    [
+      {
+        "ssid": "mz_airport_box1_2.4ghz",
+        "psk": "chrome4all",
+        "band": "2.4",
+        "tags": ["router_quality_high"]
+      },
+      {
+        "ssid": "mz_airport_box1_5ghz",
+        "psk": "chrome4all",
+        "band": "5",
+        "tags": ["router_quality_high"]
+      }
+    ],
+    "device_ids":
+    [
+      "100.107.130.216",
+      "100.107.130.217",
+      "100.107.130.219",
+      "100.107.130.222"
+    ]
+  },
+  "box2":
+  {
+    "networks":
+    [
+      {
+        "ssid": "mz_airport_box2_2.4ghz",
+        "psk": "chrome4all",
+        "band": "2.4",
+        "tags": ["router_quality_high"]
+      },
+      {
+        "ssid": "mz_airport_box2_5ghz",
+        "psk": "chrome4all",
+        "band": "5",
+        "tags": ["router_quality_high"]
+      }
+    ],
+    "device_ids":
+    [
+      "100.107.130.226",
+      "100.107.130.228",
+      "100.107.130.229",
+      "100.107.130.230"
+    ]
+  }
+}
diff --git a/lab_system/master/config/tests/multizone_sw/multizone_sw.cfg b/lab_system/master/config/tests/multizone_sw/multizone_sw.cfg
new file mode 100644
index 0000000..891def9
--- /dev/null
+++ b/lab_system/master/config/tests/multizone_sw/multizone_sw.cfg
@@ -0,0 +1,23 @@
+# Config file for multizone tests that can be run with only the receivers
+# and no additional equipment (power or network switches, access points, etc.).
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.multizone.test_cases_sw
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MultizoneGroupStatusTestCase,MultizoneGroupVolumeMultizoneNamespaceTestCase,MultizoneGroupVolumeReceiverNamespaceTestCase,MultizoneMultipleVolumeControllersMultizoneTestCase,MultizoneMultipleVolumeControllersReceiverTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.multizone.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MultizoneTestSequence
+
+# Path in the media server to the audio file to play in the group.
+TESTFILE_PATH=static/multizone/tone_440hz_f48k_d60sec.mp3
+# Duration of the media in seconds.
+TESTFILE_LENGTH_SECS=10
+# Mime type of the media file.
+TESTFILE_MIME_TYPE=audio/mp3
+
+
diff --git a/lab_system/master/config/tests/ndm/crash_loop.cfg b/lab_system/master/config/tests/ndm/crash_loop.cfg
new file mode 100644
index 0000000..6da7edd
--- /dev/null
+++ b/lab_system/master/config/tests/ndm/crash_loop.cfg
@@ -0,0 +1,13 @@
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=experimental.ndm.crash_loop_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CrashLoopTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=experimental.ndm.ota_install_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OtaInstallableTestSequence
diff --git a/lab_system/master/config/tests/ndm/crash_loop_battery_mode.cfg b/lab_system/master/config/tests/ndm/crash_loop_battery_mode.cfg
new file mode 100644
index 0000000..2eb7d09
--- /dev/null
+++ b/lab_system/master/config/tests/ndm/crash_loop_battery_mode.cfg
@@ -0,0 +1,13 @@
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=experimental.ndm.crash_loop_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=BatteryModeCrashLoopTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=experimental.ndm.ota_install_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OtaInstallableTestSequence
diff --git a/lab_system/master/config/tests/ndm/fdr.cfg b/lab_system/master/config/tests/ndm/fdr.cfg
new file mode 100644
index 0000000..f1547af
--- /dev/null
+++ b/lab_system/master/config/tests/ndm/fdr.cfg
@@ -0,0 +1,13 @@
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=experimental.ndm.fdr_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FdrTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=experimental.ndm.ndm_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NdmTestSequence
diff --git a/lab_system/master/config/tests/ndm/fdr_ndm.cfg b/lab_system/master/config/tests/ndm/fdr_ndm.cfg
new file mode 100644
index 0000000..ad290d8
--- /dev/null
+++ b/lab_system/master/config/tests/ndm/fdr_ndm.cfg
@@ -0,0 +1,13 @@
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=experimental.ndm.fdr_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=NdmFdrTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=experimental.ndm.ndm_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NdmTestSequence
diff --git a/lab_system/master/config/tests/ndm/metrics.cfg b/lab_system/master/config/tests/ndm/metrics.cfg
new file mode 100644
index 0000000..24b77e9
--- /dev/null
+++ b/lab_system/master/config/tests/ndm/metrics.cfg
@@ -0,0 +1,13 @@
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=experimental.ndm.metrics_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=MetricsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=experimental.ndm.ndm_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NdmTestSequence
diff --git a/lab_system/master/config/tests/ndm/ota_installable.cfg b/lab_system/master/config/tests/ndm/ota_installable.cfg
new file mode 100644
index 0000000..9ae3f42
--- /dev/null
+++ b/lab_system/master/config/tests/ndm/ota_installable.cfg
@@ -0,0 +1,13 @@
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=experimental.ndm.ota_installable_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OtaInstallableTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=experimental.ndm.ota_install_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OtaInstallableTestSequence
diff --git a/lab_system/master/config/tests/ndm/ota_installable_battery_mode.cfg b/lab_system/master/config/tests/ndm/ota_installable_battery_mode.cfg
new file mode 100644
index 0000000..6f1e645
--- /dev/null
+++ b/lab_system/master/config/tests/ndm/ota_installable_battery_mode.cfg
@@ -0,0 +1,13 @@
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=experimental.ndm.ota_installable_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=BatteryModeOtaInstallableTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=experimental.ndm.ota_install_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OtaInstallableTestSequence
diff --git a/lab_system/master/config/tests/ndm/ota_update.cfg b/lab_system/master/config/tests/ndm/ota_update.cfg
new file mode 100644
index 0000000..0e3236c
--- /dev/null
+++ b/lab_system/master/config/tests/ndm/ota_update.cfg
@@ -0,0 +1,16 @@
+# Config file for OTA update test suite on master branch.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=experimental.ndm.ota_update_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OtaUpdateTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=experimental.ndm.ndm_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NdmTestSequence
+
diff --git a/lab_system/master/config/tests/ndm/ota_update_network_disconnect.cfg b/lab_system/master/config/tests/ndm/ota_update_network_disconnect.cfg
new file mode 100644
index 0000000..f586228
--- /dev/null
+++ b/lab_system/master/config/tests/ndm/ota_update_network_disconnect.cfg
@@ -0,0 +1,16 @@
+# Config file for OTA update test suite on master branch.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=experimental.ndm.ota_update_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OtaUpdateNetworkDisconnectTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=experimental.ndm.ndm_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NdmTestSequence
+
diff --git a/lab_system/master/config/tests/ndm/wifi_connectivity.cfg b/lab_system/master/config/tests/ndm/wifi_connectivity.cfg
new file mode 100644
index 0000000..d6bbbe2
--- /dev/null
+++ b/lab_system/master/config/tests/ndm/wifi_connectivity.cfg
@@ -0,0 +1,13 @@
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=experimental.ndm.wifi_interface_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=WifiInterfaceConnectivityTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=experimental.ndm.ndm_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=NdmTestSequence
diff --git a/lab_system/master/config/tests/ndm_integration/ndm_integration.cfg b/lab_system/master/config/tests/ndm_integration/ndm_integration.cfg
new file mode 100644
index 0000000..8d0d34d
--- /dev/null
+++ b/lab_system/master/config/tests/ndm_integration/ndm_integration.cfg
@@ -0,0 +1,16 @@
+# Configuration values for ndm integration test.
+#
+# PROPERTIES
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=NDMIntegrationTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ndm_integration.ndm_integration_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=CopyCurlTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.ndm_integration.copy_curl_test_sequence
+
+
diff --git a/lab_system/master/config/tests/octane/octane.cfg b/lab_system/master/config/tests/octane/octane.cfg
new file mode 100644
index 0000000..5a73ab1
--- /dev/null
+++ b/lab_system/master/config/tests/octane/octane.cfg
@@ -0,0 +1,16 @@
+# Config file for Octane benchmark tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.octane.octane_benchmark_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OctaneBenchmarkTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.octane.octane_benchmark_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OctaneBenchmarkTestSequence
+
diff --git a/lab_system/master/config/tests/octane/test_defs.json b/lab_system/master/config/tests/octane/test_defs.json
new file mode 100644
index 0000000..d215d61
--- /dev/null
+++ b/lab_system/master/config/tests/octane/test_defs.json
@@ -0,0 +1,105 @@
+[
+  {
+    "group_name": "octane",
+    "slave_name": "octane",
+    "test_name": "octane",
+    "identifier": "octane",
+    "device_type": "eureka",
+    "device_group": "eureka_debug",
+    "branch": ["master", "release", "next_release", "on_demand"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "octane",
+    "slave_name": "octane_v2",
+    "test_name": "octane",
+    "identifier": "octane_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "branch": ["master", "release", "next_release"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "octane",
+    "slave_name": "octane_v3",
+    "test_name": "octane",
+    "identifier": "octane_v3",
+    "device_type": "earth",
+    "device_group": "earth_eth",
+    "branch": ["master", "release", "next_release"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "octane",
+    "slave_name": "octane_v4",
+    "test_name": "octane",
+    "identifier": "octane_v4",
+    "device_type": "luther_b2",
+    "device_group": "luther_eth",
+    "branch": ["master", "release", "next_release"],
+    "build_flavor": "user"
+  },
+  {
+    "group_name": "octane",
+    "slave_name": "eurtest_tv",
+    "test_name": "octane",
+    "identifier": "octane_bubbletea",
+    "device_type": "bubbletea",
+    "device_group": "bubbletea",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  },
+  {
+    "group_name": "octane",
+    "slave_name": "eurtest_tv",
+    "test_name": "octane",
+    "identifier": "octane_entourage",
+    "device_type": "dragonfly",
+    "device_group": "entourage",
+    "branch": ["cast_tv_dragonfly_master", "cast_tv_dragonfly_release", "cast_tv_dragonfly_next_release"]
+  },
+  {
+    "group_name": "octane",
+    "slave_name": "eurtest_tv",
+    "test_name": "octane",
+    "identifier": "octane_guabao",
+    "device_type": "guabao",
+    "device_group": "entourage",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  },
+  {
+    "group_name": "octane",
+    "slave_name": "eurtest_tv",
+    "test_name": "octane",
+    "identifier": "octane_mars_us",
+    "device_type": "Mars_US",
+    "device_group": "entourage",
+    "branch": ["cast_tv_mediatek_master", "cast_tv_mediatek_release", "cast_tv_mediatek_next_release"]
+  },
+  {
+    "group_name": "octane",
+    "slave_name": "eurtest_tv",
+    "test_name": "octane",
+    "identifier": "octane_N00007",
+    "device_type": "N00007",
+    "device_group": "entourage",
+    "branch": ["cast_tv_novatek_master", "cast_tv_novatek_release", "cast_tv_novatek_next_release"]
+  },
+  {
+    "group_name": "octane",
+    "slave_name": "eurtest_tv",
+    "test_name": "octane",
+    "identifier": "octane_suncake",
+    "device_type": "suncake",
+    "device_group": "entourage",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  },
+  {
+    "group_name": "octane",
+    "slave_name": "eurtest_tv",
+    "test_name": "octane",
+    "identifier": "octane_taroball",
+    "device_type": "taroball",
+    "device_group": "entourage",
+    "branch": ["cast_tv_mstar_master", "cast_tv_mstar_release", "cast_tv_mstar_next_release"]
+  }
+]
diff --git a/lab_system/master/config/tests/ota_clearspace/ota_clearspace.cfg b/lab_system/master/config/tests/ota_clearspace/ota_clearspace.cfg
new file mode 100644
index 0000000..e59a940
--- /dev/null
+++ b/lab_system/master/config/tests/ota_clearspace/ota_clearspace.cfg
@@ -0,0 +1,21 @@
+# Config file for OTA clear space test suite on master branch.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ota_clearspace.ota_clearspace
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OtaClearSpaceTestCase,OtaNotEnoughSpaceTestCase,OtaDownloadInterruptTestCase,OtaServerInterruptTestCase,OtaCorruptFileDownloadTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingCollectLogcatTestSequence
+
+# Get second to latest build
+BUILD_SELECTION_POLICY=second_to_latest
+
+# OTA update master version. fetcher will use the value to download latest OTA.
+BRANCH_TO_TEST=master
diff --git a/lab_system/master/config/tests/ota_installable/atv_ota_installable.cfg b/lab_system/master/config/tests/ota_installable/atv_ota_installable.cfg
new file mode 100644
index 0000000..77d821b
--- /dev/null
+++ b/lab_system/master/config/tests/ota_installable/atv_ota_installable.cfg
@@ -0,0 +1,21 @@
+# Config file for the OTA installable tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ota_installable.android_ota_installable_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AndroidOtaInstallableTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.android_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidTestSequence
+
+
diff --git a/lab_system/master/config/tests/ota_installable/fuchsia_ota_installable.cfg b/lab_system/master/config/tests/ota_installable/fuchsia_ota_installable.cfg
new file mode 100644
index 0000000..edf8a73
--- /dev/null
+++ b/lab_system/master/config/tests/ota_installable/fuchsia_ota_installable.cfg
@@ -0,0 +1,21 @@
+# Config file for the Fuchsia OTA installable tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ota_installable.fuchsia_ota_installable_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FuchsiaOtaInstallableTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.ota_installable.fuchsia_ota_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaOtaInstallableTestSequence
+
+
diff --git a/lab_system/master/config/tests/ota_installable/ota_installable.cfg b/lab_system/master/config/tests/ota_installable/ota_installable.cfg
new file mode 100644
index 0000000..321652b
--- /dev/null
+++ b/lab_system/master/config/tests/ota_installable/ota_installable.cfg
@@ -0,0 +1,21 @@
+# Config file for the OTA installable tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ota_installable.ota_installable_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OtaInstallableTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.ota_installable.ota_installable_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OtaInstallableTestSequence
+
+
diff --git a/lab_system/master/config/tests/ota_installable/test_defs.json b/lab_system/master/config/tests/ota_installable/test_defs.json
new file mode 100644
index 0000000..7647b08
--- /dev/null
+++ b/lab_system/master/config/tests/ota_installable/test_defs.json
@@ -0,0 +1,293 @@
+[
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "eng",
+        "device_group": "ota_test",
+        "device_type": "eureka",
+        "group_name": "ota_installable",
+        "identifier": "anchovy_ota_eng",
+        "scheduler": "55 * * * *",
+        "slave_name": "anchovy_ota_eng",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "user",
+        "device_group": "ota_test",
+        "device_type": "eureka",
+        "group_name": "ota_installable",
+        "identifier": "anchovy_ota_user",
+        "scheduler": "50 * * * *",
+        "slave_name": "anchovy_ota_user",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "eng",
+        "device_group": "ota_test",
+        "device_type": "lexx",
+        "group_name": "ota_installable",
+        "identifier": "salami_ota_eng",
+        "scheduler": "45 * * * *",
+        "slave_name": "salami_ota_eng",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "user",
+        "device_group": "ota_test",
+        "device_type": "lexx",
+        "group_name": "ota_installable",
+        "identifier": "salami_ota_user",
+        "num_devices": "1",
+        "scheduler": "40 * * * *",
+        "slave_name": "salami_ota_user",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "eng",
+        "device_group": "ota_test",
+        "device_type": "luther_b2",
+        "group_name": "ota_installable",
+        "identifier": "luther_ota_eng",
+        "scheduler": "35 * * * *",
+        "slave_name": "luther_ota_eng",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "user",
+        "device_group": "ota_test",
+        "device_type": "luther_b2",
+        "group_name": "ota_installable",
+        "identifier": "luther_ota_user",
+        "scheduler": "30 * * * *",
+        "slave_name": "luther_ota_user",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release",
+            "1_28a"
+        ],
+        "build_flavor": "eng",
+        "device_group": "ota_test",
+        "device_type": "chirp",
+        "group_name": "ota_installable",
+        "identifier": "pineapple_ota_eng",
+        "scheduler": "25 * * * *",
+        "slave_name": "pineapple_ota_eng",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release",
+            "1_28a"
+        ],
+        "build_flavor": "user",
+        "device_group": "ota_test",
+        "device_type": "chirp",
+        "group_name": "ota_installable",
+        "identifier": "pineapple_ota_user",
+        "scheduler": "20 * * * *",
+        "slave_name": "pineapple_ota_user",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "eng",
+        "device_group": "ota_test",
+        "device_type": "hendrix",
+        "group_name": "ota_installable",
+        "identifier": "pepperoni_ota_eng",
+        "scheduler": "15 * * * *",
+        "slave_name": "pepperoni_ota_eng",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "user",
+        "device_group": "ota_test",
+        "device_type": "hendrix",
+        "group_name": "ota_installable",
+        "identifier": "pepperoni_ota_user",
+        "scheduler": "10 * * * *",
+        "slave_name": "pepperoni_ota_user",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "eng",
+        "device_group": "ota_test",
+        "device_type": "earth",
+        "group_name": "ota_installable",
+        "identifier": "chorizo_ota_eng",
+        "scheduler": "5 * * * *",
+        "slave_name": "chorizo_ota_eng",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "user",
+        "device_group": "ota_test",
+        "device_type": "earth",
+        "group_name": "ota_installable",
+        "identifier": "chorizo_ota_user",
+        "scheduler": "0 * * * *",
+        "slave_name": "chorizo_ota_user",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "eng",
+        "device_group": "ota_test",
+        "device_type": "assistantdefault",
+        "group_name": "ota_installable",
+        "identifier": "assistantdefault_ota_eng",
+        "scheduler": "55 * * * *",
+        "slave_name": "assistantdefault_ota_eng",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "user",
+        "device_group": "ota_test",
+        "device_type": "assistantdefault",
+        "group_name": "ota_installable",
+        "identifier": "assistantdefault_ota_user",
+        "scheduler": "50 * * * *",
+        "slave_name": "assistantdefault_ota_user",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "eng",
+        "device_group": "ota_test",
+        "device_type": "joplin",
+        "group_name": "ota_installable",
+        "identifier": "joplin_ota_eng",
+        "scheduler": "55 * * * *",
+        "slave_name": "joplin_ota_eng",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "user",
+        "device_group": "ota_test",
+        "device_type": "joplin",
+        "group_name": "ota_installable",
+        "identifier": "joplin_ota_user",
+        "scheduler": "50 * * * *",
+        "slave_name": "joplin_ota_user",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "eng",
+        "device_group": "ota_test",
+        "device_type": "biggie",
+        "group_name": "ota_installable",
+        "identifier": "biggie_ota_eng",
+        "scheduler": "0 * * * *",
+        "slave_name": "biggie_ota_eng",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    },
+    {
+        "branch": [
+            "master",
+            "release",
+            "next_release"
+        ],
+        "build_flavor": "user",
+        "device_group": "ota_test",
+        "device_type": "biggie",
+        "group_name": "ota_installable",
+        "identifier": "biggie_ota_user",
+        "scheduler": "5 * * * *",
+        "slave_name": "biggie_ota_user",
+        "step_factories_recipe": "step_factories_recipes/ota_installable_test_recipe.json",
+        "test_name": "ota_installable"
+    }
+]
diff --git a/lab_system/master/config/tests/ota_update/ota_update.cfg b/lab_system/master/config/tests/ota_update/ota_update.cfg
new file mode 100644
index 0000000..b9d8d3a
--- /dev/null
+++ b/lab_system/master/config/tests/ota_update/ota_update.cfg
@@ -0,0 +1,23 @@
+# Config file for OTA update test suite on master branch.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ota_update.ota_update_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OtaUpdateTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
+
+# Get second to latest build
+BUILD_SELECTION_POLICY=second_to_latest
+
+# OTA update master version. fetcher will use the value to download latest OTA.
+BRANCH_TO_TEST=master
+
+
diff --git a/lab_system/master/config/tests/ota_update/ota_update_factory.cfg b/lab_system/master/config/tests/ota_update/ota_update_factory.cfg
new file mode 100644
index 0000000..f2f0521
--- /dev/null
+++ b/lab_system/master/config/tests/ota_update/ota_update_factory.cfg
@@ -0,0 +1,21 @@
+# Config file for OTA update test suite on 1.27 factory branch.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ota_update.ota_update_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OtaUpdateTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
+
+# Get second to latest build
+BUILD_SELECTION_POLICY=second_to_latest
+
+# OTA update factory version. fetcher will use the value to download latest OTA.
+BRANCH_TO_TEST=1.27f
diff --git a/lab_system/master/config/tests/ota_update/ota_update_master.cfg b/lab_system/master/config/tests/ota_update/ota_update_master.cfg
new file mode 100644
index 0000000..b9d8d3a
--- /dev/null
+++ b/lab_system/master/config/tests/ota_update/ota_update_master.cfg
@@ -0,0 +1,23 @@
+# Config file for OTA update test suite on master branch.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ota_update.ota_update_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OtaUpdateTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
+
+# Get second to latest build
+BUILD_SELECTION_POLICY=second_to_latest
+
+# OTA update master version. fetcher will use the value to download latest OTA.
+BRANCH_TO_TEST=master
+
+
diff --git a/lab_system/master/config/tests/ota_update/ota_update_release.cfg b/lab_system/master/config/tests/ota_update/ota_update_release.cfg
new file mode 100644
index 0000000..0d4e246
--- /dev/null
+++ b/lab_system/master/config/tests/ota_update/ota_update_release.cfg
@@ -0,0 +1,20 @@
+# Config file for OTA update test suite on release branches.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ota_update.ota_update_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OtaUpdateTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
+
+# Get second to latest build
+BUILD_SELECTION_POLICY=second_to_latest
+
+
diff --git a/lab_system/master/config/tests/perception_activity_zones/perception_activity_zones.cfg b/lab_system/master/config/tests/perception_activity_zones/perception_activity_zones.cfg
new file mode 100644
index 0000000..028bca6
--- /dev/null
+++ b/lab_system/master/config/tests/perception_activity_zones/perception_activity_zones.cfg
@@ -0,0 +1,24 @@
+#
+#Configuration values for the event detection test.
+#
+# PROPERTIES
+
+API_PATH=stellar
+GCS_BUCKET=catatester_test_deps
+GCS_VIDEO_DIR_PATH=perception/activity_zones
+ITERATIONS=1
+USER=cast.test.dev23@gmail.com
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PerceptionActivityZonesTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.perception_activity_zones.perception_activity_zones_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=PerceptionEventDetectionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.perception_event_detection.perception_event_detection_test_sequence
+
+
+
diff --git a/lab_system/master/config/tests/perception_event_detection/perception_event_detection.cfg b/lab_system/master/config/tests/perception_event_detection/perception_event_detection.cfg
new file mode 100644
index 0000000..ab8f7c9
--- /dev/null
+++ b/lab_system/master/config/tests/perception_event_detection/perception_event_detection.cfg
@@ -0,0 +1,24 @@
+#
+#Configuration values for the event detection test.
+#
+# PROPERTIES
+
+API_PATH=stellar
+GCS_BUCKET=catatester_test_deps
+GCS_VIDEO_DIR_PATH=perception/event_detection
+ITERATIONS=1
+USER=cast.test.dev23@gmail.com
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PerceptionEventDetectionTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.perception_event_detection.perception_event_detection_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=PerceptionEventDetectionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.perception_event_detection.perception_event_detection_test_sequence
+
+
+
diff --git a/lab_system/master/config/tests/perception_face_detection/perception_face_detection.cfg b/lab_system/master/config/tests/perception_face_detection/perception_face_detection.cfg
new file mode 100644
index 0000000..2b9040f
--- /dev/null
+++ b/lab_system/master/config/tests/perception_face_detection/perception_face_detection.cfg
@@ -0,0 +1,24 @@
+#
+#Configuration values for the face detection test.
+#
+# PROPERTIES
+
+API_PATH=stellar
+GCS_BUCKET=catatester_test_deps
+GCS_VIDEO_DIR_PATH=perception/familiar_face
+ITERATIONS=1
+USER=cast.test.dev23@gmail.com
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PerceptionFaceDetectionTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.perception_face_detection.perception_face_detection_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=PerceptionEventDetectionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.perception_event_detection.perception_event_detection_test_sequence
+
+
+
diff --git a/lab_system/master/config/tests/perception_latency/perception_latency.cfg b/lab_system/master/config/tests/perception_latency/perception_latency.cfg
new file mode 100644
index 0000000..e4bdd14
--- /dev/null
+++ b/lab_system/master/config/tests/perception_latency/perception_latency.cfg
@@ -0,0 +1,24 @@
+# Configuration values for the perception latency test.
+#
+# PROPERTIES
+
+API_PATH=stellar
+DEVICE_ENROLL_DIR=/data/chrome/chirp/gallery
+ENROLLMENT_DIR_PATH=perception/enrollment_gallery
+GCS_BUCKET=catatester_test_deps
+GCS_VIDEO_DIR_PATH=perception/test_videos/motion
+ITERATIONS=15
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PerceptionLatencyTestCaseMotion
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.perception_latency.perception_latency_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=PerceptionLatencyTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.perception_latency.perception_latency_test_sequence
+
+
+
diff --git a/lab_system/master/config/tests/perception_latency/perception_latency_fuchsia.cfg b/lab_system/master/config/tests/perception_latency/perception_latency_fuchsia.cfg
new file mode 100644
index 0000000..63f7165
--- /dev/null
+++ b/lab_system/master/config/tests/perception_latency/perception_latency_fuchsia.cfg
@@ -0,0 +1,25 @@
+# Configuration values for the perception latency test.
+#
+# PROPERTIES
+
+API_PATH=ml_framework
+API_PORT=8805
+DEVICE_ENROLL_DIR=/data/persistent/253a01f8ffb9be3d543731594c39c80b3548cb953319cdbc719fb7ffc7f32ed2/kronk/gallery
+ENROLLMENT_DIR_PATH=perception/enrollment_gallery
+GCS_BUCKET=catatester_test_deps
+GCS_VIDEO_DIR_PATH=perception/test_videos/motion
+ITERATIONS=25
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PerceptionLatencyTestCaseMotion
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.perception_latency.perception_latency_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaPerceptionLatencyTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.perception_latency.fuchsia_perception_latency_test_sequence
+
+
+
diff --git a/lab_system/master/config/tests/perception_latency_ndm/perception_latency.cfg b/lab_system/master/config/tests/perception_latency_ndm/perception_latency.cfg
new file mode 100644
index 0000000..538e26a
--- /dev/null
+++ b/lab_system/master/config/tests/perception_latency_ndm/perception_latency.cfg
@@ -0,0 +1,25 @@
+# Configuration values for the perception latency test.
+#
+# PROPERTIES
+
+API_PATH=stellar
+DEVICE_ENROLL_DIR=/data/chrome/chirp/gallery
+ENROLLMENT_DIR_PATH=perception/enrollment_gallery
+GCS_BUCKET=catatester_test_deps
+GCS_VIDEO_DIR_PATH=perception/test_videos/motion
+ITERATIONS=1
+POWER_MODE=line_power
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PerceptionLatencyTestCaseMotion
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.perception_latency_ndm.perception_latency_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=PerceptionLatencyTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.perception_latency_ndm.perception_latency_test_sequence
+
+
+
diff --git a/lab_system/master/config/tests/perception_latency_ndm/perception_latency_battery.cfg b/lab_system/master/config/tests/perception_latency_ndm/perception_latency_battery.cfg
new file mode 100644
index 0000000..f189a3a
--- /dev/null
+++ b/lab_system/master/config/tests/perception_latency_ndm/perception_latency_battery.cfg
@@ -0,0 +1,25 @@
+# Configuration values for the perception latency test.
+#
+# PROPERTIES
+
+API_PATH=stellar
+DEVICE_ENROLL_DIR=/data/chrome/chirp/gallery
+ENROLLMENT_DIR_PATH=perception/enrollment_gallery
+GCS_BUCKET=catatester_test_deps
+GCS_VIDEO_DIR_PATH=perception/test_videos/motion
+ITERATIONS=1
+POWER_MODE=battery
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PerceptionLatencyTestCaseMotion
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.perception_latency_ndm.perception_latency_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=PerceptionLatencyTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.perception_latency_ndm.perception_latency_test_sequence
+
+
+
diff --git a/lab_system/master/config/tests/perception_stability/perception_stability.cfg b/lab_system/master/config/tests/perception_stability/perception_stability.cfg
new file mode 100644
index 0000000..5d864a9
--- /dev/null
+++ b/lab_system/master/config/tests/perception_stability/perception_stability.cfg
@@ -0,0 +1,25 @@
+#
+#Configuration values for the perception stability test.
+#
+# PROPERTIES
+
+API_PATH=stellar
+GCS_BUCKET=catatester_test_deps
+GCS_VIDEO_DIR_PATH=perception/stability
+ITERATIONS=1
+SCENE_TYPE=motion
+USER=cast.test.dev23@gmail.com
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PerceptionStabilityTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.perception_stability.perception_stability_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=PerceptionEventDetectionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.perception_event_detection.perception_event_detection_test_sequence
+
+
+
diff --git a/lab_system/master/config/tests/perception_stability/perception_stability_static.cfg b/lab_system/master/config/tests/perception_stability/perception_stability_static.cfg
new file mode 100644
index 0000000..de1c3c1
--- /dev/null
+++ b/lab_system/master/config/tests/perception_stability/perception_stability_static.cfg
@@ -0,0 +1,25 @@
+#
+#Configuration values for the perception stability test.
+#
+# PROPERTIES
+
+API_PATH=stellar
+GCS_BUCKET=catatester_test_deps
+GCS_VIDEO_DIR_PATH=perception/stability_static_person
+ITERATIONS=1
+SCENE_TYPE=static
+USER=cast.test.dev23@gmail.com
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PerceptionStabilityTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.perception_stability.perception_stability_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=PerceptionEventDetectionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.perception_event_detection.perception_event_detection_test_sequence
+
+
+
diff --git a/lab_system/master/config/tests/perception_stability/perception_stability_static_person.cfg b/lab_system/master/config/tests/perception_stability/perception_stability_static_person.cfg
new file mode 100644
index 0000000..cf37a14
--- /dev/null
+++ b/lab_system/master/config/tests/perception_stability/perception_stability_static_person.cfg
@@ -0,0 +1,25 @@
+#
+#Configuration values for the perception stability test.
+#
+# PROPERTIES
+
+API_PATH=stellar
+GCS_BUCKET=catatester_test_deps
+GCS_VIDEO_DIR_PATH=perception/stability_static_person
+ITERATIONS=1
+SCENE_TYPE=static_person
+USER=cast.test.dev23@gmail.com
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PerceptionStabilityTestCase
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.perception_stability.perception_stability_test_case
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=PerceptionEventDetectionTestSequence
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.perception_event_detection.perception_event_detection_test_sequence
+
+
+
diff --git a/lab_system/master/config/tests/pog/audio_formats.cfg b/lab_system/master/config/tests/pog/audio_formats.cfg
new file mode 100644
index 0000000..d1ddd89
--- /dev/null
+++ b/lab_system/master/config/tests/pog/audio_formats.cfg
@@ -0,0 +1,15 @@
+# Config file for JavaScript audio format tests.
+#
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.audio_formats.audio_formats_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SupportedAudioFormatsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+DISABLED_TESTS=SupportedAudioFormatsHiResTestCase,AudioBitRatesTestCase
diff --git a/lab_system/master/config/tests/pog/properties.cfg b/lab_system/master/config/tests/pog/properties.cfg
new file mode 100644
index 0000000..cce5bc1
--- /dev/null
+++ b/lab_system/master/config/tests/pog/properties.cfg
@@ -0,0 +1,21 @@
+# Config file for properties tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.properties.properties_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PropertiesTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingCollectLogcatTestSequence
+
+
diff --git a/lab_system/master/config/tests/pog/reboot_metrics.cfg b/lab_system/master/config/tests/pog/reboot_metrics.cfg
new file mode 100644
index 0000000..8675c9e
--- /dev/null
+++ b/lab_system/master/config/tests/pog/reboot_metrics.cfg
@@ -0,0 +1,14 @@
+# Config file for testing RebootLoop.
+#
+# PROPERTIES
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.reboot_timer.reboot_metrics_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=RebootMetricsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
diff --git a/lab_system/master/config/tests/pog/receiver_c4a.cfg b/lab_system/master/config/tests/pog/receiver_c4a.cfg
new file mode 100644
index 0000000..722a5cd
--- /dev/null
+++ b/lab_system/master/config/tests/pog/receiver_c4a.cfg
@@ -0,0 +1,47 @@
+# Config file for the Cast receiver tests on audio devices
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.cast.receiver.compliance_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CastReceiverMirroringProtocolTestCase,CastReceiverStressTestCase,CastReceiverMediaProtocolMP4TestCase,CastReceiverMediaProtocolSmoothStreamingTestCase,CastReceiverBenchmarkTestCase,CastReceiverQueueTestCase,CastReceiverCapabilityTestCase,CastReceiverMediaProtocolHLSTestCase,CastReceiverMediaProtocolDASHTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Receiver app ids:
+CAST_V2_DEBUG_APP_ID=C566BECD
+CAST_V2_PLAYER_APP_ID=BE6E4473
+CHROME_MIRRORING_APP_ID=85CDB22F
+
+# Test specific configuration values:
+MP4_MEDIA_URL=https://storage.googleapis.com/cast_media/format_tests/audio/stereo_guitar.mp3
+MP4_MEDIA_MIME_TYPE=video/mp4
+
+# Temporary fix
+DASH_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/dash/sample-dash-list.mpd
+DASH_MEDIA_MIME_TYPE=application/dash+xml
+HLS_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/hls/bbb/bbb_hls.m3u8
+HLS_MEDIA_MIME_TYPE=application/x-mpegurl
+SMOOTH_STREAMING_MEDIA_URL=https://home-media-server.sandbox.google.com/static/receiver/smooth/BBB_H264_AACL/big_buck_bunny_1080p_stereo.ism/manifest
+SMOOTH_STREAMING_MEDIA_MIME_TYPE=application/vnd.ms-sstr+xml
+
+MEDIA_LOAD_TIMEOUT_SECS=30
+
+# Flag to skip tests that require a display and can't be run in C4A devices.
+NO_DISPLAY=1
+
+# This config is used to determine if cast operations should be allowed even if
+# device is not signed-in using a gaia account.
+ENABLE_CAST_WITHOUT_SIGN_IN=True
+
+
diff --git a/lab_system/master/config/tests/pog/setup_wifi_channel.cfg b/lab_system/master/config/tests/pog/setup_wifi_channel.cfg
new file mode 100644
index 0000000..f5ce52c
--- /dev/null
+++ b/lab_system/master/config/tests/pog/setup_wifi_channel.cfg
@@ -0,0 +1,22 @@
+# Config file for setup wifi channel tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup.setup_wifi_channel_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SetupWifiChannelTestCase,SetupWifiChannel5GHzTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
+
+
+
diff --git a/lab_system/master/config/tests/pog/wifi_interface_connectivity.cfg b/lab_system/master/config/tests/pog/wifi_interface_connectivity.cfg
new file mode 100644
index 0000000..88b47d2
--- /dev/null
+++ b/lab_system/master/config/tests/pog/wifi_interface_connectivity.cfg
@@ -0,0 +1,13 @@
+# PROPERTIES
+#
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup_api_wifi.wifi_interface_connectivity_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=WifiInterfaceConnectivityTestCase,WifiInterfaceRebootTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.setup_api_wifi.wifi_interface_connectivity_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=WifiConnectivityTestSequence
diff --git a/lab_system/master/config/tests/presence_detection/presence_detection.cfg b/lab_system/master/config/tests/presence_detection/presence_detection.cfg
new file mode 100644
index 0000000..a811e03
--- /dev/null
+++ b/lab_system/master/config/tests/presence_detection/presence_detection.cfg
@@ -0,0 +1,33 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.presence_detection.presence_detection_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PresenceDetectionTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.assistant.presence_detection.presence_detection_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=PresenceDetectionTestSequence
+
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/presence_detection_samples
+
+# Use Tunnel where applicable
+
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
+# Threshold and timeout value for test
+PRESENCE_DETECTION_THRESHOLD=0.3
+PRESENCE_DETECTION_TIMEOUT=10
+
+# Endpoint API path
+API_PATH=stellar/presence
\ No newline at end of file
diff --git a/lab_system/master/config/tests/preview_test/preview_test.cfg b/lab_system/master/config/tests/preview_test/preview_test.cfg
new file mode 100644
index 0000000..5e895f1
--- /dev/null
+++ b/lab_system/master/config/tests/preview_test/preview_test.cfg
@@ -0,0 +1,16 @@
+# Config file for preview channel test suite.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.preview.preview_channel_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PreviewChannelTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.preview.preview_channel_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=PreviewChannelTestSequence
+
diff --git a/lab_system/master/config/tests/preview_test/test_defs.json b/lab_system/master/config/tests/preview_test/test_defs.json
new file mode 100644
index 0000000..f7e6b50
--- /dev/null
+++ b/lab_system/master/config/tests/preview_test/test_defs.json
@@ -0,0 +1,12 @@
+[
+  {
+    "group_name": "preview_test",
+    "slave_name": "preview_test",
+    "test_name": "preview_test",
+    "identifier": "preview_test",
+    "device_type": ["earth", "hendrix"],
+    "device_group": "preview_dvt",
+    "branch": ["master", "release"],
+    "build_flavor": "user"
+  }
+]
diff --git a/lab_system/master/config/tests/properties/properties.cfg b/lab_system/master/config/tests/properties/properties.cfg
new file mode 100644
index 0000000..cce5bc1
--- /dev/null
+++ b/lab_system/master/config/tests/properties/properties.cfg
@@ -0,0 +1,21 @@
+# Config file for properties tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.properties.properties_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=PropertiesTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingCollectLogcatTestSequence
+
+
diff --git a/lab_system/master/config/tests/proximity/proximity_observer_fan.cfg b/lab_system/master/config/tests/proximity/proximity_observer_fan.cfg
new file mode 100644
index 0000000..646881c
--- /dev/null
+++ b/lab_system/master/config/tests/proximity/proximity_observer_fan.cfg
@@ -0,0 +1,41 @@
+# Config file for proximity test using Fan
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.proximity.proximity_fan_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ProximityFanTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.proximity.proximity_fan_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=ProximityFanTestSequence
+
+# Use Tunnel where applicable
+USE_TUNNEL=False
+
+#Fan Socket Number
+SOCKET_ONE_P_FIVE_FEET=1
+SOCKET_ONE_P_FIVE_FEET_PASSRATE=98
+SOCKET_THREE_FEET=2
+SOCKET_THREE_FEET_PASSRATE=98
+SOCKET_SEVEN_FEET=3
+SOCKET_SEVEN_FEET_PASSRATE=98
+
+#Test Duration
+TEST_DURATION_HOUR=0.5
+
+AUDIO_PATH=https://home-media-server.sandbox.google.com/static/proximity_background.mp3
+AUDIO_TYPE=audio/mp3
+
+#Detection Type
+DETECTION_TYPE=observer
+
+SIGNED_IN=True
\ No newline at end of file
diff --git a/lab_system/master/config/tests/proximity/proximity_observer_negative.cfg b/lab_system/master/config/tests/proximity/proximity_observer_negative.cfg
new file mode 100644
index 0000000..0581b13
--- /dev/null
+++ b/lab_system/master/config/tests/proximity/proximity_observer_negative.cfg
@@ -0,0 +1,53 @@
+# Config file for proximity test using Fan
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.proximity.proximity_neg_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ProximityNegTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.proximity.proximity_neg_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=ProximityNegTestSequence
+
+# Use Tunnel where applicable
+USE_TUNNEL=False
+
+PASSRATE=98
+
+TEST_VOLUME_NUM=4
+# Play Back Volume
+PLAYBACK_VOLUME_1=0.1
+PLAYBACK_VOLUME_2=0.4
+PLAYBACK_VOLUME_3=0.8
+PLAYBACK_VOLUME_4=1.0
+
+#Test Duration
+TEST_DURATION_HOUR=0.2
+
+#Detection Type
+DETECTION_TYPE=observer
+
+TEST_AUDIO_NUM=10
+TEST_AUDIO_1=https://home-media-server.sandbox.google.com/static/proximity_neg/Bad_Guy.mp3
+TEST_AUDIO_2=https://home-media-server.sandbox.google.com/static/proximity_neg/Beautiful_People.mp3
+TEST_AUDIO_3=https://home-media-server.sandbox.google.com/static/proximity_neg/Boy_Friend.mp3
+TEST_AUDIO_4=https://home-media-server.sandbox.google.com/static/proximity_neg/Fading.mp3
+TEST_AUDIO_5=https://home-media-server.sandbox.google.com/static/proximity_neg/Going_Bad.mp3
+TEST_AUDIO_6=https://home-media-server.sandbox.google.com/static/proximity_neg/I_Donot_Care.mp3
+TEST_AUDIO_7=https://home-media-server.sandbox.google.com/static/proximity_neg/Panini.mp3
+TEST_AUDIO_8=https://home-media-server.sandbox.google.com/static/proximity_neg/Ransom.mp3
+TEST_AUDIO_9=https://home-media-server.sandbox.google.com/static/proximity_neg/Sucker.mp3
+TEST_AUDIO_10=https://home-media-server.sandbox.google.com/static/proximity_neg/Without_Me.mp3
+
+AUDIO_TYPE=audio/mp3
+
+SIGNED_IN=True
\ No newline at end of file
diff --git a/lab_system/master/config/tests/proximity/proximity_observer_skim.cfg b/lab_system/master/config/tests/proximity/proximity_observer_skim.cfg
new file mode 100644
index 0000000..7c77f1e
--- /dev/null
+++ b/lab_system/master/config/tests/proximity/proximity_observer_skim.cfg
@@ -0,0 +1,50 @@
+# Config file for proximity test using Fan
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.proximity.proximity_skim_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ProximitySkimTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.proximity.proximity_skim_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=ProximitySkimTestSequence
+
+# Use Tunnel where applicable
+USE_TUNNEL=False
+
+# In Range / Out of Range Pass Rate
+PLAYBACK_VOLUME_1_PASSRATE=99
+PLAYBACK_VOLUME_2_PASSRATE=99
+PLAYBACK_VOLUME_3_PASSRATE=99
+PLAYBACK_VOLUME_4_PASSRATE=99
+
+#Test Duration
+TEST_DURATION_HOUR=0.05
+
+AUDIO_PATH=https://home-media-server.sandbox.google.com/static/proximity_neg/Panini.mp3
+AUDIO_TYPE=audio/mp3
+
+#Detection Type
+DETECTION_TYPE=observer
+
+# Play Back Volume
+TEST_VOLUME_NUM=4
+PLAYBACK_VOLUME_1=0.1
+PLAYBACK_VOLUME_2=0.4
+PLAYBACK_VOLUME_3=0.8
+PLAYBACK_VOLUME_4=1.0
+
+ITERATION=100
+
+BUFFER_CLEANUP_DELAY_SEC=25
+
+SIGNED_IN=True
\ No newline at end of file
diff --git a/lab_system/master/config/tests/query_reboot_loop/query_reboot_loop.cfg b/lab_system/master/config/tests/query_reboot_loop/query_reboot_loop.cfg
new file mode 100644
index 0000000..e089230
--- /dev/null
+++ b/lab_system/master/config/tests/query_reboot_loop/query_reboot_loop.cfg
@@ -0,0 +1,18 @@
+# Config file for testing RebootLoop.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.query_reboot_loop.query_reboot_loop_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=QueryRebootLoopTestCase,QueryRebootLoopWifiTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.query_reboot_loop.query_reboot_loop_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=QueryRebootLoopTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_marketing_golden/follow_up/answers
\ No newline at end of file
diff --git a/lab_system/master/config/tests/reboot_loop/reboot_loop.cfg b/lab_system/master/config/tests/reboot_loop/reboot_loop.cfg
new file mode 100644
index 0000000..b4d99b8
--- /dev/null
+++ b/lab_system/master/config/tests/reboot_loop/reboot_loop.cfg
@@ -0,0 +1,15 @@
+# Config file for testing RebootLoop.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.reboot_loop.reboot_loop_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=RebootLoopTestCase,RebootLoopWifiTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.reboot_loop.reboot_loop_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=RebootLoopTestSequence
\ No newline at end of file
diff --git a/lab_system/master/config/tests/reboot_metrics/reboot_metrics.cfg b/lab_system/master/config/tests/reboot_metrics/reboot_metrics.cfg
new file mode 100644
index 0000000..8675c9e
--- /dev/null
+++ b/lab_system/master/config/tests/reboot_metrics/reboot_metrics.cfg
@@ -0,0 +1,14 @@
+# Config file for testing RebootLoop.
+#
+# PROPERTIES
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.reboot_timer.reboot_metrics_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=RebootMetricsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
diff --git a/lab_system/master/config/tests/reboot_timer/dumpstate_timer.cfg b/lab_system/master/config/tests/reboot_timer/dumpstate_timer.cfg
new file mode 100644
index 0000000..b73dbf3
--- /dev/null
+++ b/lab_system/master/config/tests/reboot_timer/dumpstate_timer.cfg
@@ -0,0 +1,15 @@
+# Configuration values for TimeDumpstateTestCase.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.reboot_timer.dumpstate_timer_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=DumpstateTimerTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
diff --git a/lab_system/master/config/tests/reboot_timer/reboot_timer.cfg b/lab_system/master/config/tests/reboot_timer/reboot_timer.cfg
new file mode 100644
index 0000000..d916265
--- /dev/null
+++ b/lab_system/master/config/tests/reboot_timer/reboot_timer.cfg
@@ -0,0 +1,17 @@
+# Configuration values for the reboot_timer test.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.reboot_timer.reboot_timer_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=RebootTimeMeasurementTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
+
+
diff --git a/lab_system/master/config/tests/reboot_timer/reboot_timer_c4a.cfg b/lab_system/master/config/tests/reboot_timer/reboot_timer_c4a.cfg
new file mode 100644
index 0000000..c70f41a
--- /dev/null
+++ b/lab_system/master/config/tests/reboot_timer/reboot_timer_c4a.cfg
@@ -0,0 +1,16 @@
+# Configuration values for the reboot_timer test for audio devices.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.reboot_timer.reboot_timer_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=RebootTimeMeasurementTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
+
+
diff --git a/lab_system/master/config/tests/reboot_timer/test_defs.json b/lab_system/master/config/tests/reboot_timer/test_defs.json
new file mode 100644
index 0000000..378041f
--- /dev/null
+++ b/lab_system/master/config/tests/reboot_timer/test_defs.json
@@ -0,0 +1,29 @@
+[
+  {
+    "group_name": "reboot_timer",
+    "slave_name": "reboot_timer_v3",
+    "test_name": "reboot_timer",
+    "identifier": "reboot_timer_entourage",
+    "device_type": "dragonfly",
+    "device_group": "entourage",
+    "branch": "cast_tv_dragonfly_master"
+  },
+  {
+    "group_name": "reboot_timer",
+    "slave_name": "reboot_timer_biggie",
+    "test_name": "reboot_timer",
+    "identifier": "reboot_timer_biggie",
+    "device_type": "biggie",
+    "device_group": "biggie",
+    "branch": ["master", "release", "next_release"]
+  },
+  {
+    "group_name": "reboot_timer",
+    "slave_name": "reboot_timer_joplin",
+    "test_name": "reboot_timer",
+    "identifier": "reboot_timer_joplin",
+    "device_type": "joplin",
+    "device_group": "joplin",
+    "branch": ["master", "release", "next_release"]
+  }
+]
diff --git a/lab_system/master/config/tests/reconnect_test/reconnect_test.cfg b/lab_system/master/config/tests/reconnect_test/reconnect_test.cfg
new file mode 100644
index 0000000..f439ece
--- /dev/null
+++ b/lab_system/master/config/tests/reconnect_test/reconnect_test.cfg
@@ -0,0 +1,14 @@
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.reconnect_test.reconnect_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ReconnectTest
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
diff --git a/lab_system/master/config/tests/remote_controller/atv_remote_ota_installable.cfg b/lab_system/master/config/tests/remote_controller/atv_remote_ota_installable.cfg
new file mode 100644
index 0000000..438ad28
--- /dev/null
+++ b/lab_system/master/config/tests/remote_controller/atv_remote_ota_installable.cfg
@@ -0,0 +1,19 @@
+# Config file for the OTA installable tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.remote_controller.atv_remote_ota_installable_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AtvRemoteOtaInstallableTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.android_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidTestSequence
diff --git a/lab_system/master/config/tests/selina/fuchsia_sleep_chester.cfg b/lab_system/master/config/tests/selina/fuchsia_sleep_chester.cfg
new file mode 100644
index 0000000..22a6bcc
--- /dev/null
+++ b/lab_system/master/config/tests/selina/fuchsia_sleep_chester.cfg
@@ -0,0 +1,30 @@
+# Config file for sleep tracking test using chester
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.selina.sleep_chester_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SleepChesterTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.selina.fuchsia_sleep_chester_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaSleepChesterTestSequence
+
+SLEEP_STATE=SUBJECT_STATUS_ABSENT,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ASLEEP,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ABSENT
+SLEEP_DURATION=5,10,60,5,130
+EXPECTED_SLEEP_SESSION_NUM=1
+EXPECTED_SLEEP_LENGTH=60
+SLEEP_LENGTH_ERROR=5
+
+# refer to go/sleep_seq_test
+TEST_SEQ_NAME=Simplest_seq
+
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/selina/fuchsia_sleep_chester_1_1.cfg b/lab_system/master/config/tests/selina/fuchsia_sleep_chester_1_1.cfg
new file mode 100644
index 0000000..eaf289b
--- /dev/null
+++ b/lab_system/master/config/tests/selina/fuchsia_sleep_chester_1_1.cfg
@@ -0,0 +1,30 @@
+# Config file for sleep tracking test using chester
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.selina.sleep_chester_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SleepChesterTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.selina.fuchsia_sleep_chester_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaSleepChesterTestSequence
+
+SLEEP_STATE=SUBJECT_STATUS_ABSENT,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ASLEEP,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ASLEEP,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ABSENT,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ASLEEP,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ABSENT
+SLEEP_DURATION=5,10,60,10,30,10,10,5,20,5,130
+EXPECTED_SLEEP_SESSION_NUM=1
+EXPECTED_SLEEP_LENGTH=105
+SLEEP_LENGTH_ERROR=5
+
+# refer to go/sleep_seq_test
+TEST_SEQ_NAME=Sleep_seq_1_1
+
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/selina/fuchsia_sleep_chester_2_1.cfg b/lab_system/master/config/tests/selina/fuchsia_sleep_chester_2_1.cfg
new file mode 100644
index 0000000..1578f83
--- /dev/null
+++ b/lab_system/master/config/tests/selina/fuchsia_sleep_chester_2_1.cfg
@@ -0,0 +1,30 @@
+# Config file for sleep tracking test using chester
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.selina.sleep_chester_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SleepChesterTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.selina.fuchsia_sleep_chester_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaSleepChesterTestSequence
+
+SLEEP_STATE=SUBJECT_STATUS_ABSENT,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ASLEEP,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ABSENT,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ASLEEP,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ABSENT
+SLEEP_DURATION=5,10,60,5,130,10,50,5,130
+EXPECTED_SLEEP_SESSION_NUM=2
+EXPECTED_SLEEP_LENGTH=60,50
+SLEEP_LENGTH_ERROR=5
+
+# refer to go/sleep_seq_test
+TEST_SEQ_NAME=Sleep_seq_2_1
+
+SIGNED_IN=True
diff --git a/lab_system/master/config/tests/selina/gesture_data_injection.cfg b/lab_system/master/config/tests/selina/gesture_data_injection.cfg
new file mode 100644
index 0000000..ae86358
--- /dev/null
+++ b/lab_system/master/config/tests/selina/gesture_data_injection.cfg
@@ -0,0 +1,27 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.selina.gesture_data_injection_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=GestureDataInjectionTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.selina.gesture_data_injection_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=GestureDataInjectionTestSequence
+
+# the test audio config files.
+GCS_SELINA_DATA_DIR_PATH=assistant/selina_gesture_data
+
+# number of iteration
+TEST_ITERATION_COUNT=10
+
+SIGNED_IN=True
\ No newline at end of file
diff --git a/lab_system/master/config/tests/selina/sleep_chester.cfg b/lab_system/master/config/tests/selina/sleep_chester.cfg
new file mode 100644
index 0000000..5f75bf7
--- /dev/null
+++ b/lab_system/master/config/tests/selina/sleep_chester.cfg
@@ -0,0 +1,30 @@
+# Config file for sleep tracking test using chester
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.selina.sleep_chester_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SleepChesterTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.selina.sleep_chester_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SleepChesterTestSequence
+
+SLEEP_STATE=SUBJECT_STATUS_ABSENT,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ASLEEP,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ABSENT
+SLEEP_DURATION=5,10,60,5,130
+EXPECTED_SLEEP_SESSION_NUM=1
+EXPECTED_SLEEP_LENGTH=60
+SLEEP_LENGTH_ERROR=5
+
+# refer to go/sleep_seq_test
+TEST_SEQ_NAME=Simplest_seq
+
+SIGNED_IN=True
\ No newline at end of file
diff --git a/lab_system/master/config/tests/selina/sleep_chester_1_1.cfg b/lab_system/master/config/tests/selina/sleep_chester_1_1.cfg
new file mode 100644
index 0000000..c0e3adf
--- /dev/null
+++ b/lab_system/master/config/tests/selina/sleep_chester_1_1.cfg
@@ -0,0 +1,30 @@
+# Config file for sleep tracking test using chester
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.selina.sleep_chester_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SleepChesterTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.selina.sleep_chester_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SleepChesterTestSequence
+
+SLEEP_STATE=SUBJECT_STATUS_ABSENT,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ASLEEP,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ASLEEP,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ABSENT,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ASLEEP,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ABSENT
+SLEEP_DURATION=5,10,60,10,30,10,10,5,20,5,130
+EXPECTED_SLEEP_SESSION_NUM=1
+EXPECTED_SLEEP_LENGTH=105
+SLEEP_LENGTH_ERROR=5
+
+# refer to go/sleep_seq_test
+TEST_SEQ_NAME=Sleep_seq_1_1
+
+SIGNED_IN=True
\ No newline at end of file
diff --git a/lab_system/master/config/tests/selina/sleep_chester_2_1.cfg b/lab_system/master/config/tests/selina/sleep_chester_2_1.cfg
new file mode 100644
index 0000000..89059aa
--- /dev/null
+++ b/lab_system/master/config/tests/selina/sleep_chester_2_1.cfg
@@ -0,0 +1,30 @@
+# Config file for sleep tracking test using chester
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.selina.sleep_chester_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SleepChesterTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.selina.sleep_chester_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SleepChesterTestSequence
+
+SLEEP_STATE=SUBJECT_STATUS_ABSENT,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ASLEEP,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ABSENT,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ASLEEP,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ABSENT
+SLEEP_DURATION=5,10,60,5,130,10,50,5,130
+EXPECTED_SLEEP_SESSION_NUM=2
+EXPECTED_SLEEP_LENGTH=60,50
+SLEEP_LENGTH_ERROR=5
+
+# refer to go/sleep_seq_test
+TEST_SEQ_NAME=Sleep_seq_2_1
+
+SIGNED_IN=True
\ No newline at end of file
diff --git a/lab_system/master/config/tests/selina/sleep_data_injection.cfg b/lab_system/master/config/tests/selina/sleep_data_injection.cfg
new file mode 100644
index 0000000..97e796c
--- /dev/null
+++ b/lab_system/master/config/tests/selina/sleep_data_injection.cfg
@@ -0,0 +1,24 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.selina.sleep_data_injection_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SleepDataInjectionTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.selina.sleep_data_injection_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SleepDataInjectionTestSequence
+
+SLEEP_STATE=SUBJECT_STATUS_ABSENT,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ASLEEP,SUBJECT_STATUS_AWAKE,SUBJECT_STATUS_ABSENT
+SLEEP_DURATION=5,10,40,5,130
+
+SIGNED_IN=True
\ No newline at end of file
diff --git a/lab_system/master/config/tests/settings/atv_settings.cfg b/lab_system/master/config/tests/settings/atv_settings.cfg
new file mode 100644
index 0000000..3ec3627
--- /dev/null
+++ b/lab_system/master/config/tests/settings/atv_settings.cfg
@@ -0,0 +1,15 @@
+# Config file for Android TV settings tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.settings.atv_settings_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=AtvSettingsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.android_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidTestSequence
diff --git a/lab_system/master/config/tests/settings/settings.cfg b/lab_system/master/config/tests/settings/settings.cfg
new file mode 100644
index 0000000..bf1b02e
--- /dev/null
+++ b/lab_system/master/config/tests/settings/settings.cfg
@@ -0,0 +1,17 @@
+# Configuration values for the settings test.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.settings.settings_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SettingsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.settings.settings_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SettingsTestSequence
+
+
diff --git a/lab_system/master/config/tests/settings/test_defs.json b/lab_system/master/config/tests/settings/test_defs.json
new file mode 100644
index 0000000..3ecd6d1
--- /dev/null
+++ b/lab_system/master/config/tests/settings/test_defs.json
@@ -0,0 +1,42 @@
+[
+  {
+    "group_name": "settings",
+    "slave_name": "settings",
+    "test_name": "settings",
+    "device_type": "eureka",
+    "device_group": "eureka_debug",
+    "identifier": "settings",
+    "hidden": "true",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "settings",
+    "slave_name": "settings_v2",
+    "test_name": "settings",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "identifier": "settings_v2",
+    "hidden": "true",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "settings",
+    "slave_name": "settings_v3",
+    "test_name": "settings",
+    "device_type": "earth",
+    "device_group": "earth_eth",
+    "identifier": "settings_v3",
+    "hidden": "true",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "settings",
+    "slave_name": "settings",
+    "test_name": "settings",
+    "device_type": "luther_b2",
+    "device_group": "luther_eth",
+    "identifier": "settings_v4",
+    "branch": ["master", "release"],
+    "step_factories_recipe": "step_factories_recipes/default_with_new_device_allocation.json"
+  }
+]
diff --git a/lab_system/master/config/tests/setup/atv_setup.cfg b/lab_system/master/config/tests/setup/atv_setup.cfg
new file mode 100644
index 0000000..b38540c
--- /dev/null
+++ b/lab_system/master/config/tests/setup/atv_setup.cfg
@@ -0,0 +1,15 @@
+# Config file for Cast Shell APK setup tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup.atv_setup_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SetupTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.android_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidTestSequence
\ No newline at end of file
diff --git a/lab_system/master/config/tests/setup/fuchsia_setup.cfg b/lab_system/master/config/tests/setup/fuchsia_setup.cfg
new file mode 100644
index 0000000..d632c87
--- /dev/null
+++ b/lab_system/master/config/tests/setup/fuchsia_setup.cfg
@@ -0,0 +1,19 @@
+# Config file for setup tests on Fuchsia
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup.setup_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SetupTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.fuchsia_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaTestSequence
diff --git a/lab_system/master/config/tests/setup/setup.cfg b/lab_system/master/config/tests/setup/setup.cfg
new file mode 100644
index 0000000..af3db39
--- /dev/null
+++ b/lab_system/master/config/tests/setup/setup.cfg
@@ -0,0 +1,31 @@
+# Config file for setup tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup.setup_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SetupTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Wifi ssid
+WIFI_SSID=setup_ap
+
+# Wifi Wpa Auth
+WPA_AUTH=7
+
+# Wifi Wpa Cipher
+WPA_CIPHER=4
+
+# Wifi password
+WIFI_PASSWD=/MagicRoom5!
diff --git a/lab_system/master/config/tests/setup_api_wifi/fuchsia_setup_api_wifi.cfg b/lab_system/master/config/tests/setup_api_wifi/fuchsia_setup_api_wifi.cfg
new file mode 100644
index 0000000..7f27ab9
--- /dev/null
+++ b/lab_system/master/config/tests/setup_api_wifi/fuchsia_setup_api_wifi.cfg
@@ -0,0 +1,19 @@
+# Config file for setup tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup_api_wifi.fuchsia_setup_api_wifi_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FuchsiaSetupApiWifiTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.fuchsia_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaTestSequence
diff --git a/lab_system/master/config/tests/setup_api_wifi/setup_api_wifi.cfg b/lab_system/master/config/tests/setup_api_wifi/setup_api_wifi.cfg
new file mode 100644
index 0000000..1e014a4
--- /dev/null
+++ b/lab_system/master/config/tests/setup_api_wifi/setup_api_wifi.cfg
@@ -0,0 +1,19 @@
+# Config file for setup tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup_api_wifi.setup_api_wifi_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SetupApiWifiTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingCollectLogcatTestSequence
diff --git a/lab_system/master/config/tests/setup_reboot/fuchsia_setup_reboot.cfg b/lab_system/master/config/tests/setup_reboot/fuchsia_setup_reboot.cfg
new file mode 100644
index 0000000..6b21d12
--- /dev/null
+++ b/lab_system/master/config/tests/setup_reboot/fuchsia_setup_reboot.cfg
@@ -0,0 +1,21 @@
+# Config file for setup reboot tests on Fuchsia devices
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup.fuchsia_setup_reboot_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=FuchsiaSetupRebootTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.fuchsia_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaTestSequence
+
+
diff --git a/lab_system/master/config/tests/setup_reboot/setup_reboot.cfg b/lab_system/master/config/tests/setup_reboot/setup_reboot.cfg
new file mode 100644
index 0000000..bd43770
--- /dev/null
+++ b/lab_system/master/config/tests/setup_reboot/setup_reboot.cfg
@@ -0,0 +1,21 @@
+# Config file for setup reboot tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup.setup_reboot_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SetupRebootTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingCollectLogcatTestSequence
+
+
diff --git a/lab_system/master/config/tests/setup_wifi_channel/setup_wifi_channel.cfg b/lab_system/master/config/tests/setup_wifi_channel/setup_wifi_channel.cfg
new file mode 100644
index 0000000..f5ce52c
--- /dev/null
+++ b/lab_system/master/config/tests/setup_wifi_channel/setup_wifi_channel.cfg
@@ -0,0 +1,22 @@
+# Config file for setup wifi channel tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup.setup_wifi_channel_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SetupWifiChannelTestCase,SetupWifiChannel5GHzTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
+
+
+
diff --git a/lab_system/master/config/tests/short_audio_play/short_audio_play.cfg b/lab_system/master/config/tests/short_audio_play/short_audio_play.cfg
new file mode 100644
index 0000000..0aeb13c
--- /dev/null
+++ b/lab_system/master/config/tests/short_audio_play/short_audio_play.cfg
@@ -0,0 +1,18 @@
+# Config file for testing RebootLoop.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.short_audio_play.short_audio_play_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ShortAudioPlayTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
+
+# Plot data
+PLOT=False
\ No newline at end of file
diff --git a/lab_system/master/config/tests/simple_command/simple_command.cfg b/lab_system/master/config/tests/simple_command/simple_command.cfg
new file mode 100644
index 0000000..33ec1dd
--- /dev/null
+++ b/lab_system/master/config/tests/simple_command/simple_command.cfg
@@ -0,0 +1,15 @@
+# Configuration values for the simple command test.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.simple_command.simple_command_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SimpleCommandTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.disabled_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=DisabledLoggingTestSequence
diff --git a/lab_system/master/config/tests/simple_query/simple_query.cfg b/lab_system/master/config/tests/simple_query/simple_query.cfg
new file mode 100644
index 0000000..af47f77
--- /dev/null
+++ b/lab_system/master/config/tests/simple_query/simple_query.cfg
@@ -0,0 +1,26 @@
+# Config file for testing SimpleQuery.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.simple_query.simple_query_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SimpleQueryTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.simple_query.simple_query_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SimpleQueryTestSequence
+
+# Relative GCS path (from the cast-test-data bucket) to the directory containing
+GCS_AUDIO_DIR_PATH=assistant/pineapple-p2-rerecordings/synthetic_speech/chirp_marketing_golden/follow_up/answers
+
+# Assistant backend server to use. Valid values are :
+# "NIGHTLY_DEV_SERVER", "TEAMFOOD_SERVER", "DOGFOOD_SERVER" and "PROD_SERVER"
+# These values are defined in
+# continuous-tests/internal/eurtest/device/assistant_device.py
+ASSISTANT_BACKEND=DOGFOOD_SERVER
+
+SIGNED_IN=True
\ No newline at end of file
diff --git a/lab_system/master/config/tests/smoke_tests/crash_loop.cfg b/lab_system/master/config/tests/smoke_tests/crash_loop.cfg
new file mode 100644
index 0000000..cf4b1b7
--- /dev/null
+++ b/lab_system/master/config/tests/smoke_tests/crash_loop.cfg
@@ -0,0 +1,17 @@
+# Config file for the Crash/Boot loop tests
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ota_installable.crash_loop_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=CrashLoopTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.ota_installable.ota_installable_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OtaInstallableTestSequence
+
+
diff --git a/lab_system/master/config/tests/smoke_tests/ota_installable.cfg b/lab_system/master/config/tests/smoke_tests/ota_installable.cfg
new file mode 100644
index 0000000..321652b
--- /dev/null
+++ b/lab_system/master/config/tests/smoke_tests/ota_installable.cfg
@@ -0,0 +1,21 @@
+# Config file for the OTA installable tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.ota_installable.ota_installable_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=OtaInstallableTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.ota_installable.ota_installable_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=OtaInstallableTestSequence
+
+
diff --git a/lab_system/master/config/tests/sound_sensing/sound_sensing.cfg b/lab_system/master/config/tests/sound_sensing/sound_sensing.cfg
new file mode 100644
index 0000000..26deb0d
--- /dev/null
+++ b/lab_system/master/config/tests/sound_sensing/sound_sensing.cfg
@@ -0,0 +1,30 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.sound_sensing.sound_sensing_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SoundSensingTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.sound_sensing.sound_sensing_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SoundSensingTestSequence
+
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/sound_sensing_recordings
+
+# number of iteration
+TEST_ITERATION_COUNT=5
+
+# Use Tunnel where applicable
+
+TCP_TUNNEL_TIMEOUT_SECS=1800
+
diff --git a/lab_system/master/config/tests/sound_sensing/sound_sensing_mic_writer.cfg b/lab_system/master/config/tests/sound_sensing/sound_sensing_mic_writer.cfg
new file mode 100644
index 0000000..8fb4c46
--- /dev/null
+++ b/lab_system/master/config/tests/sound_sensing/sound_sensing_mic_writer.cfg
@@ -0,0 +1,28 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.sound_sensing.sound_sensing_mic_writer_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SoundSensingMicWriterTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.sound_sensing.sound_sensing_mic_writer_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SoundSensingMicWriterTestSequence
+
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/sound_sensing_recordings
+
+# number of iteration
+TEST_ITERATION_COUNT=10
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
diff --git a/lab_system/master/config/tests/sound_sensing/sound_sensing_mic_writer_p1.cfg b/lab_system/master/config/tests/sound_sensing/sound_sensing_mic_writer_p1.cfg
new file mode 100644
index 0000000..307f964
--- /dev/null
+++ b/lab_system/master/config/tests/sound_sensing/sound_sensing_mic_writer_p1.cfg
@@ -0,0 +1,28 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.sound_sensing.sound_sensing_mic_writer_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SoundSensingMicWriterTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.sound_sensing.sound_sensing_mic_writer_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SoundSensingMicWriterTestSequence
+
+# the test audio config files for P1 recordings.
+GCS_AUDIO_DIR_PATH=assistant/sound_sensing_recordings_p1
+
+# number of iteration
+TEST_ITERATION_COUNT=10
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
diff --git a/lab_system/master/config/tests/sound_sensing/sound_sensing_mic_writer_p2.cfg b/lab_system/master/config/tests/sound_sensing/sound_sensing_mic_writer_p2.cfg
new file mode 100644
index 0000000..dc7827a
--- /dev/null
+++ b/lab_system/master/config/tests/sound_sensing/sound_sensing_mic_writer_p2.cfg
@@ -0,0 +1,28 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.sound_sensing.sound_sensing_mic_writer_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SoundSensingMicWriterTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.sound_sensing.sound_sensing_mic_writer_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SoundSensingMicWriterTestSequence
+
+# the test audio config files for P2 recordings.
+GCS_AUDIO_DIR_PATH=assistant/sound_sensing_recordings_p2
+
+# number of iteration
+TEST_ITERATION_COUNT=10
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
diff --git a/lab_system/master/config/tests/sound_sensing/sound_sensing_mic_writer_small_pool.cfg b/lab_system/master/config/tests/sound_sensing/sound_sensing_mic_writer_small_pool.cfg
new file mode 100644
index 0000000..32f068d
--- /dev/null
+++ b/lab_system/master/config/tests/sound_sensing/sound_sensing_mic_writer_small_pool.cfg
@@ -0,0 +1,28 @@
+# Config file for presence test using conducted auido
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.sound_sensing.sound_sensing_mic_writer_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SoundSensingMicWriterTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.sound_sensing.sound_sensing_mic_writer_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=SoundSensingMicWriterTestSequence
+
+# the test audio config files.
+GCS_AUDIO_DIR_PATH=assistant/sound_sensing_recordings_small_pool
+
+# number of iteration
+TEST_ITERATION_COUNT=10
+
+# Use Tunnel where applicable
+TCP_TUNNEL_TIMEOUT_SECS=1800
diff --git a/lab_system/master/config/tests/system_perf/system_perf_duo_video_call.cfg b/lab_system/master/config/tests/system_perf/system_perf_duo_video_call.cfg
new file mode 100644
index 0000000..fe42e7e
--- /dev/null
+++ b/lab_system/master/config/tests/system_perf/system_perf_duo_video_call.cfg
@@ -0,0 +1,20 @@
+# Config file for system perf duo video call tests.
+# In this test case, we collect system performance metrics for a duo video call.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.system_perf.duo_video_call_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SystemPerfDuoVideoCallTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Max duo call duration for testing in seconds
+MAX_DUO_CALL_DURATION_SECS=30
+
diff --git a/lab_system/master/config/tests/system_perf/system_perf_long_duo_video_call.cfg b/lab_system/master/config/tests/system_perf/system_perf_long_duo_video_call.cfg
new file mode 100644
index 0000000..39320d4
--- /dev/null
+++ b/lab_system/master/config/tests/system_perf/system_perf_long_duo_video_call.cfg
@@ -0,0 +1,26 @@
+# Config file for system perf duo video call tests.
+# In this test case, we collect system performance metrics for a duo video call.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.system_perf.long_duo_video_call_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SystemPerfLongDuoVideoCallTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Duo testing code Bot ID
+DUO_CALL_BOT_ID=bot:duo-bot-router-gfile-bot+/x20/teams/home-newman-ee/duo-bot/pinknoise.mkv+loop
+
+# Max duration in seconds for each duo call in the test
+MAX_DUO_CALL_DURATION_SECS=600
+
+# Number of the Duo calls being made in the test
+DUO_CALL_REPEAT_TIMES=12
+
diff --git a/lab_system/master/config/tests/system_perf/system_perf_long_duo_video_call_short.cfg b/lab_system/master/config/tests/system_perf/system_perf_long_duo_video_call_short.cfg
new file mode 100644
index 0000000..6ec9092
--- /dev/null
+++ b/lab_system/master/config/tests/system_perf/system_perf_long_duo_video_call_short.cfg
@@ -0,0 +1,26 @@
+# Config file for system perf duo video call tests.
+# In this test case, we collect system performance metrics for a duo video call.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.system_perf.long_duo_video_call_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SystemPerfLongDuoVideoCallTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Duo testing code Bot ID
+DUO_CALL_BOT_ID=bot:duo-bot-router-gfile-bot+/x20/teams/home-newman-ee/duo-bot/pinknoise.mkv+loop
+
+# Max duration in seconds for each duo call in the test
+MAX_DUO_CALL_DURATION_SECS=300
+
+# Number of the Duo calls being made in the test
+DUO_CALL_REPEAT_TIMES=1
+
diff --git a/lab_system/master/config/tests/system_perf/system_perf_sound_sensing.cfg b/lab_system/master/config/tests/system_perf/system_perf_sound_sensing.cfg
new file mode 100644
index 0000000..fba5419
--- /dev/null
+++ b/lab_system/master/config/tests/system_perf/system_perf_sound_sensing.cfg
@@ -0,0 +1,20 @@
+# Config file for system perf duo video call tests.
+# In this test case, we collect system performance metrics for a duo video call.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.system_perf.sound_sensing_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SystemPerfSoundSensingTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Total Wait time in seconds to measure the system performance
+SOUND_SENSING_WAIT_TIME_SECS=60
+
diff --git a/lab_system/master/config/tests/system_perf/system_perf_video_playback.cfg b/lab_system/master/config/tests/system_perf/system_perf_video_playback.cfg
new file mode 100644
index 0000000..c74e80e
--- /dev/null
+++ b/lab_system/master/config/tests/system_perf/system_perf_video_playback.cfg
@@ -0,0 +1,41 @@
+# Config file for system perf video playback tests.
+# In this test case, we collect system performance metrics when playing different video playbacks.
+# The video formats include youtube video and web media from different codec.
+# Including 4k video test on 4k supported devices.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.system_perf.video_playback_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SystemPerfVideoPlaybackTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
+
+# Test case timeout secs
+TEST_CASE_TIMEOUT_SECS=1800
+
+# Video start timeout secs
+VIDEO_START_TIMEOUT_SECS=30
+
+# The number of seconds the video play.
+SHORT_VIDEO_DURATION_SECS=100
+LONG_VIDEO_DURATION_SECS=180
+
+
+# The number of seconds for cold start time. Should be long enough to prevent cast failure.
+COLD_START_TIME_SECS=8
+
+# 4K VP9 web media
+WEB_MEDIA_4K_VP9=https://storage.googleapis.com/vp9-4k-stream/phfx_4KHD_VP9TestFootage.webm
+
+# FHD web media
+WEB_MEDIA_FHD=https://home-media-server.sandbox.google.com/static/media/car-1920x1080.mp4
+
+# 4K Youtube video
+YOUTUBE_4K_VIDEO_ID=ftlvreFtA2A
diff --git a/lab_system/master/config/tests/system_perf/system_perf_video_playback_android.cfg b/lab_system/master/config/tests/system_perf/system_perf_video_playback_android.cfg
new file mode 100644
index 0000000..40d5187
--- /dev/null
+++ b/lab_system/master/config/tests/system_perf/system_perf_video_playback_android.cfg
@@ -0,0 +1,28 @@
+# Config file for system perf video playback tests.
+# In this test case, we collect system performance metrics when playing YouTube
+# video on Android devices.
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.system_perf.android_youtube_playback_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SystemPerfAndroidYoutubePlaybackTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.android_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidTestSequence
+
+# Test case timeout secs
+TEST_CASE_TIMEOUT_SECS=1800
+
+# The number of seconds the video play.
+VIDEO_DURATION_SECS=120
+
+# The number of seconds for cold start time. Should be long enough for the
+# video to start.
+COLD_START_TIME_SECS=8
+
+# Target Video on YouTube
+YOUTUBE_VIDEO_ID=ftlvreFtA2A
diff --git a/lab_system/master/config/tests/test_beds/eraser_biggie_100_115_57_2.json b/lab_system/master/config/tests/test_beds/eraser_biggie_100_115_57_2.json
new file mode 100644
index 0000000..16a855a
--- /dev/null
+++ b/lab_system/master/config/tests/test_beds/eraser_biggie_100_115_57_2.json
@@ -0,0 +1,26 @@
+{
+    "rpi_hotword": {
+        "host": "100.115.57.4",
+        "username": "pi",
+        "password": "casting",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "hotword_file": "/home/pi/test_tones/okgoogle-ken_trim.wav",
+        "hotword_level_file": "/home/pi/test_tones/okgoogle-ken_trim_6_repeat.wav",
+        "ok_google_samples_dir" : "/home/pi/ok_google_samples",
+        "hey_google_samples_dir" : "/home/pi/hey_google_samples",
+        "speaker_volume": "100",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "rpi_chirp": {
+        "host": "100.115.57.3",
+        "username": "pi",
+        "password": "casting",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "speaker_volume": "100",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "chirp": {
+        "host": "100.115.57.2"
+    }
+}
+
diff --git a/lab_system/master/config/tests/test_beds/eraser_biggie_100_115_60_69.json b/lab_system/master/config/tests/test_beds/eraser_biggie_100_115_60_69.json
new file mode 100644
index 0000000..2625b11
--- /dev/null
+++ b/lab_system/master/config/tests/test_beds/eraser_biggie_100_115_60_69.json
@@ -0,0 +1,24 @@
+{
+    "rpi_hotword": {
+        "host": "100.115.60.70",
+        "username": "pi",
+        "password": "casting",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "hotword_file": "/home/pi/test_tones/okgoogle-ken_trim.wav",
+        "hotword_level_file": "/home/pi/test_tones/okgoogle-ken_trim_6_repeat.wav",
+        "ok_google_samples_dir" : "/home/pi/ok_google_samples",
+        "hey_google_samples_dir" : "/home/pi/hey_google_samples",
+        "speaker_volume": "95"
+    },
+    "dut_sound_meter_host": {
+        "host": "100.115.60.71",
+        "username": "pi",
+        "password": "casting",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "speaker_volume": "95"
+    },
+    "chirp": {
+        "host": "100.115.60.69"
+    }
+}
+
diff --git a/lab_system/master/config/tests/test_beds/eraser_chirp_100_115_121_11.json b/lab_system/master/config/tests/test_beds/eraser_chirp_100_115_121_11.json
new file mode 100644
index 0000000..f3fb6c4
--- /dev/null
+++ b/lab_system/master/config/tests/test_beds/eraser_chirp_100_115_121_11.json
@@ -0,0 +1,26 @@
+{
+    "rpi_hotword": {
+        "host": "100.115.121.4",
+        "username": "pi",
+        "password": "casting",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "hotword_file": "/home/pi/test_tones/okgoogle-ken_trim.wav",
+        "hotword_level_file": "/home/pi/test_tones/okgoogle-ken_trim_6_repeat.wav",
+        "ok_google_samples_dir" : "/home/pi/ok_google_samples",
+        "hey_google_samples_dir" : "/home/pi/hey_google_samples",
+        "speaker_volume": "95",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "rpi_chirp": {
+        "host": "100.115.121.4",
+        "username": "pi",
+        "password": "casting",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "speaker_volume": "95",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "chirp": {
+        "host": "100.115.121.11"
+    }
+}
+
diff --git a/lab_system/master/config/tests/test_beds/eraser_joplin_100_115_121_9.json b/lab_system/master/config/tests/test_beds/eraser_joplin_100_115_121_9.json
new file mode 100644
index 0000000..7d0834b
--- /dev/null
+++ b/lab_system/master/config/tests/test_beds/eraser_joplin_100_115_121_9.json
@@ -0,0 +1,26 @@
+{
+    "rpi_hotword": {
+        "host": "100.115.121.5",
+        "username": "pi",
+        "password": "casting",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "hotword_file": "/home/pi/test_tones/okgoogle-ken_trim.wav",
+        "hotword_level_file": "/home/pi/test_tones/okgoogle-ken_trim_6_repeat.wav",
+        "ok_google_samples_dir" : "/home/pi/ok_google_samples",
+        "hey_google_samples_dir" : "/home/pi/hey_google_samples",
+        "speaker_volume": "95",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "rpi_chirp": {
+        "host": "100.115.121.5",
+        "username": "pi",
+        "password": "casting",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "speaker_volume": "95",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "chirp": {
+        "host": "100.115.121.9"
+    }
+}
+
diff --git a/lab_system/master/config/tests/test_beds/hotword_samples_biggie_100_115_122_19.json.disabled b/lab_system/master/config/tests/test_beds/hotword_samples_biggie_100_115_122_19.json.disabled
new file mode 100644
index 0000000..425aab6
--- /dev/null
+++ b/lab_system/master/config/tests/test_beds/hotword_samples_biggie_100_115_122_19.json.disabled
@@ -0,0 +1,27 @@
+{
+    "rpi_hotword": {
+        "host": "100.115.121.3",
+        "username": "pi",
+        "password": "casting",
+        "temp_dir": "/tmp",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "hotword_file": "/home/pi/test_tones/okgoogle-ken_trim.wav",
+        "hotword_level_file": "/home/pi/test_tones/okgoogle-ken_trim_6_repeat.wav",
+        "ok_google_samples_dir" : "/home/pi/ok_google_samples",
+        "hey_google_samples_dir" : "/home/pi/hey_google_samples",
+        "speaker_volume": "100",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "rpi_chirp": {
+        "host": "100.115.121.3",
+        "username": "pi",
+        "password": "casting",
+        "temp_dir": "/tmp",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "speaker_volume": "100",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "chirp": {
+        "host": "100.115.122.19"
+    }
+}
diff --git a/lab_system/master/config/tests/test_beds/hotword_samples_chirp_100_115_122_125.json.disabled b/lab_system/master/config/tests/test_beds/hotword_samples_chirp_100_115_122_125.json.disabled
new file mode 100644
index 0000000..394080a
--- /dev/null
+++ b/lab_system/master/config/tests/test_beds/hotword_samples_chirp_100_115_122_125.json.disabled
@@ -0,0 +1,27 @@
+{
+    "rpi_hotword": {
+        "host": "100.115.122.126",
+        "username": "pi",
+        "password": "casting",
+        "temp_dir": "/tmp",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "hotword_file": "/home/pi/test_tones/okgoogle-ken_trim.wav",
+        "hotword_level_file": "/home/pi/test_tones/okgoogle-ken_trim_6_repeat.wav",
+        "ok_google_samples_dir" : "/home/pi/ok_google_samples",
+        "hey_google_samples_dir" : "/home/pi/hey_google_samples",
+        "speaker_volume": "100",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "rpi_chirp": {
+        "host": "100.115.122.126",
+        "username": "pi",
+        "password": "casting",
+        "temp_dir": "/tmp",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "speaker_volume": "100",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "chirp": {
+        "host": "100.115.122.125"
+    }
+}
diff --git a/lab_system/master/config/tests/test_beds/hotword_samples_estelle_100_115_122_93.json.disabled b/lab_system/master/config/tests/test_beds/hotword_samples_estelle_100_115_122_93.json.disabled
new file mode 100644
index 0000000..637e714
--- /dev/null
+++ b/lab_system/master/config/tests/test_beds/hotword_samples_estelle_100_115_122_93.json.disabled
@@ -0,0 +1,27 @@
+{
+   "rpi_hotword": {
+       "host": "100.115.122.97",
+       "username": "pi",
+       "password": "casting",
+       "temp_dir": "/tmp",
+       "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+       "hotword_file": "/home/pi/test_tones/okgoogle-ken_trim.wav",
+       "hotword_level_file": "/home/pi/test_tones/okgoogle-ken_trim_6_repeat.wav",
+       "ok_google_samples_dir" : "/home/pi/ok_google_samples",
+       "hey_google_samples_dir" : "/home/pi/hey_google_samples",
+       "speaker_volume": "100",
+       "record_device": "sysdefault:CARD=1"
+   },
+   "rpi_chirp": {
+       "host": "100.115.122.97",
+       "username": "pi",
+       "password": "casting",
+       "temp_dir": "/tmp",
+       "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+       "speaker_volume": "100",
+       "record_device": "sysdefault:CARD=1"
+   },
+   "chirp": {
+       "host": "100.115.122.93"
+   }
+}
diff --git a/lab_system/master/config/tests/test_beds/hotword_samples_joplin_100_115_120_255.json.disabled b/lab_system/master/config/tests/test_beds/hotword_samples_joplin_100_115_120_255.json.disabled
new file mode 100644
index 0000000..95ec3dd
--- /dev/null
+++ b/lab_system/master/config/tests/test_beds/hotword_samples_joplin_100_115_120_255.json.disabled
@@ -0,0 +1,27 @@
+{
+    "rpi_hotword": {
+        "host": "100.115.120.254",
+        "username": "pi",
+        "password": "casting",
+        "temp_dir": "/tmp",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "hotword_file": "/home/pi/test_tones/okgoogle-ken_trim.wav",
+        "hotword_level_file": "/home/pi/test_tones/okgoogle-ken_trim_6_repeat.wav",
+        "ok_google_samples_dir" : "/home/pi/ok_google_samples",
+        "hey_google_samples_dir" : "/home/pi/hey_google_samples",
+        "speaker_volume": "100",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "rpi_chirp": {
+        "host": "100.115.120.254",
+        "username": "pi",
+        "password": "casting",
+        "temp_dir": "/tmp",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "speaker_volume": "100",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "chirp": {
+        "host": "100.115.120.255"
+    }
+}
diff --git a/lab_system/master/config/tests/test_beds/latency_biggie_100_115_121_12.json b/lab_system/master/config/tests/test_beds/latency_biggie_100_115_121_12.json
new file mode 100644
index 0000000..6278d5f
--- /dev/null
+++ b/lab_system/master/config/tests/test_beds/latency_biggie_100_115_121_12.json
@@ -0,0 +1,35 @@
+{
+    "rpi_hotword": {
+        "host": "100.115.121.6",
+        "username": "pi",
+        "password": "casting",
+        "temp_dir": "/tmp",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "hotword_file": "/home/pi/test_tones/okgoogle-ken_trim.wav",
+        "hotword_level_file": "/home/pi/test_tones/okgoogle-ken_trim_6_repeat.wav",
+        "ok_google_samples_dir" : "/home/pi/ok_google_samples",
+        "hey_google_samples_dir" : "/home/pi/hey_google_samples",
+        "speaker_volume": "80",
+        "playout_config": {
+            "card_number": 1,
+            "device_number": 0
+        },
+        "recording_config": {
+            "card_number": 1,
+            "device_number": 0
+        }
+    },
+    "rpi_chirp": {
+        "host": "100.115.121.6",
+        "username": "pi",
+        "password": "casting",
+        "temp_dir": "/tmp",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "speaker_volume": "100",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "chirp": {
+        "host": "100.115.121.12"
+    }
+}
+
diff --git a/lab_system/master/config/tests/test_beds/latency_chirp_100_115_122_90.json b/lab_system/master/config/tests/test_beds/latency_chirp_100_115_122_90.json
new file mode 100644
index 0000000..f4b6f62
--- /dev/null
+++ b/lab_system/master/config/tests/test_beds/latency_chirp_100_115_122_90.json
@@ -0,0 +1,32 @@
+{
+    "rpi_hotword": {
+        "host": "100.115.122.94",
+        "username": "pi",
+        "password": "casting",
+        "temp_dir": "/tmp",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "hotword_file": "/home/pi/test_tones/okgoogle-ken_trim.wav",
+        "hotword_level_file": "/home/pi/test_tones/okgoogle-ken_trim_6_repeat.wav",
+        "ok_google_samples_dir": "/home/pi/ok_google_samples",
+        "hey_google_samples_dir": "/home/pi/hey_google_samples",
+        "speaker_volume": "100",
+        "recording_config": {
+            "card_number": 1
+        },
+        "playout_config": {
+            "card_number": 1,
+            "device_number": 0
+        }
+    },
+    "rpi_chirp": {
+        "host": "100.115.122.94",
+        "username": "pi",
+        "password": "casting",
+        "temp_dir": "/tmp",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "speaker_volume": "100"
+    },
+    "chirp": {
+        "host": "100.115.122.90"
+    }
+}
diff --git a/lab_system/master/config/tests/test_beds/latency_imx7d_192_168_1_216.json b/lab_system/master/config/tests/test_beds/latency_imx7d_192_168_1_216.json
new file mode 100644
index 0000000..eff831c
--- /dev/null
+++ b/lab_system/master/config/tests/test_beds/latency_imx7d_192_168_1_216.json
@@ -0,0 +1,27 @@
+{
+    "rpi_hotword": {
+        "host": "192.168.1.114",
+        "username": "pi",
+        "password": "casting",
+        "temp_dir": "/tmp",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "hotword_file": "/home/pi/test_tones/okgoogle-ken_trim.wav",
+        "hotword_level_file": "/home/pi/test_tones/okgoogle-ken_trim_6_repeat.wav",
+        "ok_google_samples_dir" : "/home/pi/ok_google_samples",
+        "hey_google_samples_dir" : "/home/pi/hey_google_samples",
+        "speaker_volume": "100",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "rpi_chirp": {
+        "host": "192.168.1.114",
+        "username": "pi",
+        "password": "casting",
+        "tmp_dir": "/tmp",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "speaker_volume": "100",
+        "record_device": "sysdefault:CARD=1"
+    },
+    "chirp": {
+        "host": "192.168.1.216"
+    }
+}
diff --git a/lab_system/master/config/tests/test_beds/latency_joplin_100_115_122_91.json b/lab_system/master/config/tests/test_beds/latency_joplin_100_115_122_91.json
new file mode 100644
index 0000000..fe11487
--- /dev/null
+++ b/lab_system/master/config/tests/test_beds/latency_joplin_100_115_122_91.json
@@ -0,0 +1,27 @@
+{
+    "rpi_hotword": {
+        "host": "100.115.122.95",
+        "username": "pi",
+        "password": "casting",
+        "temp_dir": "/tmp",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "hotword_file": "/home/pi/test_tones/okgoogle-ken_trim.wav",
+        "hotword_level_file": "/home/pi/test_tones/okgoogle-ken_trim_6_repeat.wav",
+        "ok_google_samples_dir" : "/home/pi/ok_google_samples",
+        "hey_google_samples_dir" : "/home/pi/hey_google_samples",
+        "speaker_volume": "100",
+        "record_device": "sysdefault:CARD=0"
+    },
+    "rpi_chirp": {
+        "host": "100.115.122.95",
+        "username": "pi",
+        "password": "casting",
+        "temp_dir": "/tmp",
+        "sound_meter_program": "python /home/pi/sound_meter/sound_meter.py",
+        "speaker_volume": "100",
+        "record_device": "sysdefault:CARD=0"
+    },
+    "chirp": {
+        "host": "100.115.122.91"
+    }
+}
diff --git a/lab_system/master/config/tests/testingrepounittests/test_defs.json b/lab_system/master/config/tests/testingrepounittests/test_defs.json
new file mode 100644
index 0000000..bc25aa7
--- /dev/null
+++ b/lab_system/master/config/tests/testingrepounittests/test_defs.json
@@ -0,0 +1,92 @@
+[
+  {
+    "group_name": "testingrepounittests",
+    "slave_name": "testingrepounittests",
+    "test_name": "testingrepounittests",
+    "branch": "master",
+    "device_type": "dummy",
+    "identifier": "testingrepounittests",
+    "step_factories": [
+      {
+        "step_description": "Overrides property setting - not necessary for server",
+        "step_identifier": "properties",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces device allocation with a no-op",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+              "module_path": "lab_system.master.steps.factory.common.noop",
+              "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces build handling with a no-op",
+        "step_identifier": "build_handling",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces testrun setup with a no-op",
+        "step_identifier": "testrun_setup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Runs the unit tests",
+        "step_identifier": "test_running",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.run.unit",
+            "class_name": "UnitTestRunnerStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Copies logs to the Cloud Storage",
+        "step_identifier": "logs_copy",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.output.cloud_logs",
+            "class_name": "CopyLogsToRepositoryStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces output processing with a no-op",
+        "step_identifier": "output_processing",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      },
+      {
+        "step_description": "Replaces cleanup with a no-op",
+        "step_identifier": "testrun_cleanup",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.common.noop",
+            "class_name": "NoopStepFactory"
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/tests/thread/thread.cfg b/lab_system/master/config/tests/thread/thread.cfg
new file mode 100644
index 0000000..4cc3964
--- /dev/null
+++ b/lab_system/master/config/tests/thread/thread.cfg
@@ -0,0 +1,13 @@
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.thread.thread_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=ThreadTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TestSequence
diff --git a/lab_system/master/config/tests/time_bound_connectivity/time_bound_connectivity.cfg b/lab_system/master/config/tests/time_bound_connectivity/time_bound_connectivity.cfg
new file mode 100644
index 0000000..acddcac
--- /dev/null
+++ b/lab_system/master/config/tests/time_bound_connectivity/time_bound_connectivity.cfg
@@ -0,0 +1,19 @@
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.time_bound_connectivity.time_bound_connectivity_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=TimeBoundConnectivityTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.time_bound_connectivity.time_bound_connectivity_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=TimeBoundConnectivityTestSequence
+
+# Devices metadata
+SSID=test_wifi_setup
+PSK=password
+
+
diff --git a/lab_system/master/config/tests/uma_logging/mpl/test_defs.json b/lab_system/master/config/tests/uma_logging/mpl/test_defs.json
new file mode 100644
index 0000000..e8e050d
--- /dev/null
+++ b/lab_system/master/config/tests/uma_logging/mpl/test_defs.json
@@ -0,0 +1,38 @@
+[
+  {
+    "group_name": "uma_logging",
+    "slave_name": "uma_logging_mpl",
+    "test_name": "uma_logging_mpl",
+    "identifier": "uma_logging_mpl",
+    "device_type": "eureka",
+    "device_group": "eureka_background_tab_enabled",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "uma_logging",
+    "slave_name": "uma_logging_mpl",
+    "test_name": "uma_logging_mpl",
+    "identifier": "uma_logging_mpl_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_background_tab_enabled",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "uma_logging",
+    "slave_name": "uma_logging_mpl",
+    "test_name": "uma_logging_mpl",
+    "identifier": "uma_logging_mpl_v3",
+    "device_type": "earth",
+    "device_group": "earth_background_tab_enabled",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "uma_logging",
+    "slave_name": "uma_logging_mpl",
+    "test_name": "uma_logging_mpl",
+    "identifier": "uma_logging_mpl_hendrix",
+    "device_type": "hendrix",
+    "device_group": "hendrix_background_tab_enabled",
+    "branch": ["master", "release"]
+  }
+]
diff --git a/lab_system/master/config/tests/uma_logging/test_defs.json b/lab_system/master/config/tests/uma_logging/test_defs.json
new file mode 100644
index 0000000..ba10674
--- /dev/null
+++ b/lab_system/master/config/tests/uma_logging/test_defs.json
@@ -0,0 +1,84 @@
+[
+  {
+    "group_name": "uma_logging",
+    "slave_name": "uma_logging",
+    "test_name": "uma_logging",
+    "identifier": "uma_logging",
+    "device_type": "eureka",
+    "device_group": "eureka_background_tab_enabled",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "uma_logging",
+    "slave_name": "uma_logging",
+    "test_name": "uma_logging",
+    "identifier": "uma_logging_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_background_tab_enabled",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "uma_logging",
+    "slave_name": "uma_logging",
+    "test_name": "uma_logging",
+    "identifier": "uma_logging_v3",
+    "device_type": "earth",
+    "device_group": "earth_background_tab_enabled",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "uma_logging",
+    "slave_name": "uma_logging",
+    "test_name": "uma_logging",
+    "identifier": "uma_logging_hendrix",
+    "device_type": "hendrix",
+    "device_group": "hendrix_background_tab_enabled",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "uma_logging",
+    "slave_name": "uma_logging",
+    "test_name": "uma_logging_multizone",
+    "identifier": "uma_logging_multizone",
+    "device_type": "hendrix",
+    "device_group": "multizone_wifi1",
+    "num_devices": "4",
+    "scheduler": "0 0,12 * * *",
+    "branch": ["next_release"],
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "StrictDeviceAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "group_name": "uma_logging",
+    "slave_name": "uma_logging",
+    "test_name": "uma_logging_multizone",
+    "identifier": "uma_logging_multizone",
+    "device_type": "hendrix",
+    "device_group": "multizone_wifi1",
+    "num_devices": "4",
+    "scheduler": "0 6,18 * * *",
+    "branch": ["master"],
+    "step_factories": [
+      {
+        "step_description": "Allocates devices for the testrun",
+        "step_identifier": "device_allocation",
+        "step_factories": [
+          {
+            "module_path": "lab_system.master.steps.factory.device.cloud_allocator",
+            "class_name": "StrictDeviceAllocatorStepFactory"
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/lab_system/master/config/tests/uma_logging/uma_logging.cfg b/lab_system/master/config/tests/uma_logging/uma_logging.cfg
new file mode 100644
index 0000000..2ca6fc6
--- /dev/null
+++ b/lab_system/master/config/tests/uma_logging/uma_logging.cfg
@@ -0,0 +1,27 @@
+# Config file for the uma logging tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.uma_logging.uma_logging_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=UmaLoggingTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.uma_logging.uma_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=UmaLoggingTestSequence
+
+# App Ids for Testing
+BG1_APP_ID=F05C50C9
+BG2_APP_ID=C566BECD
+BACKDROP_APP_ID=E8C28D3C
+
+# Platform names
+CHROMECAST_AUDIO_PLATFORM=hendrix
diff --git a/lab_system/master/config/tests/uma_logging/uma_logging_mpl.cfg b/lab_system/master/config/tests/uma_logging/uma_logging_mpl.cfg
new file mode 100644
index 0000000..6a5f6db
--- /dev/null
+++ b/lab_system/master/config/tests/uma_logging/uma_logging_mpl.cfg
@@ -0,0 +1,22 @@
+# Config file for the uma logging tests for MPL.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.uma_logging.uma_logging_mpl_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=UmaLoggingMplTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.uma_logging.uma_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=UmaLoggingTestSequence
+
+# Platform names
+CHROMECAST_AUDIO_PLATFORM=hendrix
diff --git a/lab_system/master/config/tests/uma_logging/uma_logging_multizone.cfg b/lab_system/master/config/tests/uma_logging/uma_logging_multizone.cfg
new file mode 100644
index 0000000..0e7cd35
--- /dev/null
+++ b/lab_system/master/config/tests/uma_logging/uma_logging_multizone.cfg
@@ -0,0 +1,25 @@
+# Config file for the uma logging tests
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.uma_logging.uma_logging_multizone_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=UmaLoggingMultizoneLeaderTestCase,UmaLoggingMultizoneFollowerTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.multizone.test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=MultizoneTestSequence
+
+# Parameters for the media file to use for playback.
+MEDIA_URL=https://storage.googleapis.com/cast_media/crt_files/SarabandefromPartitaNo.1InBMinor.mp3
+MEDIA_MIME_TYPE=audio/mp3
+
+NETWORK_SWITCH_CONFIG_FILENAME=../multizone/hp_procurve_switch_config.json
diff --git a/lab_system/master/config/tests/uma_logging_wifi/test_defs.json b/lab_system/master/config/tests/uma_logging_wifi/test_defs.json
new file mode 100644
index 0000000..de361e3
--- /dev/null
+++ b/lab_system/master/config/tests/uma_logging_wifi/test_defs.json
@@ -0,0 +1,56 @@
+[
+  {
+    "group_name": "uma_logging_wifi",
+    "slave_name": "uma_logging_wifi",
+    "test_name": "uma_logging_wifi",
+    "identifier": "uma_logging_wifi",
+    "device_type": "eureka",
+    "device_group": "eureka_wifi",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "uma_logging_wifi",
+    "slave_name": "uma_logging_wifi_v2",
+    "test_name": "uma_logging_wifi",
+    "identifier": "uma_logging_wifi_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_wifi",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "uma_logging_wifi",
+    "slave_name": "uma_logging_wifi",
+    "test_name": "uma_logging_wifi",
+    "identifier": "uma_logging_wifi_v3",
+    "device_type": "earth",
+    "device_group": "earth_wifi",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "uma_logging_wifi",
+    "slave_name": "uma_logging_wifi",
+    "test_name": "uma_logging_wifi",
+    "identifier": "uma_logging_wifi_hendrix",
+    "device_type": "hendrix",
+    "device_group": "hendrix_wifi",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "uma_logging_wifi",
+    "slave_name": "uma_logging_wifi",
+    "test_name": "uma_logging_wifi",
+    "identifier": "uma_logging_wifi_chirp",
+    "device_type": "chirp",
+    "device_group": "chirp_wifi",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "uma_logging_wifi",
+    "slave_name": "uma_logging_wifi",
+    "test_name": "uma_logging_wifi",
+    "identifier": "uma_logging_wifi_v4",
+    "device_type": "luther_b2",
+    "device_group": "luther_wifi",
+    "branch": ["master", "release"]
+  }
+]
diff --git a/lab_system/master/config/tests/uma_logging_wifi/uma_logging_wifi.cfg b/lab_system/master/config/tests/uma_logging_wifi/uma_logging_wifi.cfg
new file mode 100644
index 0000000..f5cf7e2
--- /dev/null
+++ b/lab_system/master/config/tests/uma_logging_wifi/uma_logging_wifi.cfg
@@ -0,0 +1,23 @@
+# Config file for the uma logging wifi tests:
+# Those test cases will rely on devices with only wifi connection.
+#
+# Do not modify property names
+# Run make reconfig if you change a value
+# and buildbot is running
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.uma_logging.uma_logging_wifi_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=UmaLoggingWifiTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.uma_logging.uma_logging_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=UmaLoggingTestSequence
+
+AP_MAC=10:0D:7F:51:76:72
+CHANNEL=1
diff --git a/lab_system/master/config/tests/unit_test/chromium.cfg b/lab_system/master/config/tests/unit_test/chromium.cfg
new file mode 100644
index 0000000..9845432
--- /dev/null
+++ b/lab_system/master/config/tests/unit_test/chromium.cfg
@@ -0,0 +1,4 @@
+#Configuration file for Chromium test suites.
+TEST_NAME=Chromium Unit Tests
+ENABLE_FILTER_LIST=True
+EXCLUDE_SUITES_IN_LIST=True
diff --git a/lab_system/master/config/tests/unit_test/internal.cfg b/lab_system/master/config/tests/unit_test/internal.cfg
new file mode 100644
index 0000000..34df576
--- /dev/null
+++ b/lab_system/master/config/tests/unit_test/internal.cfg
@@ -0,0 +1,4 @@
+#Configuration file for internal test suites.
+TEST_NAME=Chromecast Unit Tests
+ENABLE_FILTER_LIST=True
+EXCLUDE_SUITES_IN_LIST=False
diff --git a/lab_system/master/config/tests/unit_test/internal_camera.cfg b/lab_system/master/config/tests/unit_test/internal_camera.cfg
new file mode 100644
index 0000000..029420f
--- /dev/null
+++ b/lab_system/master/config/tests/unit_test/internal_camera.cfg
@@ -0,0 +1,5 @@
+#Configuration file for internal test suites.
+TEST_NAME=Chromecast Unit Tests
+ENABLE_FILTER_LIST=True
+EXCLUDE_SUITES_IN_LIST=False
+LD_PATH=/chrome/lib
diff --git a/lab_system/master/config/tests/unit_test/internal_mclean.cfg b/lab_system/master/config/tests/unit_test/internal_mclean.cfg
new file mode 100644
index 0000000..1822c3e
--- /dev/null
+++ b/lab_system/master/config/tests/unit_test/internal_mclean.cfg
@@ -0,0 +1,4 @@
+#Configuration file for internal test suites. Different group.
+TEST_NAME=Chromecast Unit Tests
+ENABLE_FILTER_LIST=True
+EXCLUDE_SUITES_IN_LIST=False
diff --git a/lab_system/master/config/tests/video_formats/video_formats.cfg b/lab_system/master/config/tests/video_formats/video_formats.cfg
new file mode 100644
index 0000000..2d6ee5a
--- /dev/null
+++ b/lab_system/master/config/tests/video_formats/video_formats.cfg
@@ -0,0 +1,15 @@
+# Config file for JavaScript video format tests.
+#
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.video_formats.video_formats_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SupportedVideoFormatsTestCase,SupportedFormatCombinationsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+
diff --git a/lab_system/master/config/tests/video_formats/video_formats_4k.cfg b/lab_system/master/config/tests/video_formats/video_formats_4k.cfg
new file mode 100644
index 0000000..b9f5177
--- /dev/null
+++ b/lab_system/master/config/tests/video_formats/video_formats_4k.cfg
@@ -0,0 +1,15 @@
+# Config File for JavaScript UHD 4K Video Format Tests.
+#
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.video_formats.video_formats_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SupportedUHD4KVideoFormatsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+
diff --git a/lab_system/master/config/tests/video_formats/video_formats_dv.cfg b/lab_system/master/config/tests/video_formats/video_formats_dv.cfg
new file mode 100644
index 0000000..94071f2
--- /dev/null
+++ b/lab_system/master/config/tests/video_formats/video_formats_dv.cfg
@@ -0,0 +1,15 @@
+# Config File for JavaScript Dolby Vision Format Tests.
+#
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.video_formats.video_formats_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=SupportedDolbyVisionFormatsTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+
diff --git a/lab_system/master/config/tests/video_rotation_playback/video_rotation_playback.cfg b/lab_system/master/config/tests/video_rotation_playback/video_rotation_playback.cfg
new file mode 100644
index 0000000..d7d08b9
--- /dev/null
+++ b/lab_system/master/config/tests/video_rotation_playback/video_rotation_playback.cfg
@@ -0,0 +1,15 @@
+# Config file for video rotation tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.video_rotation.video_rotation_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=VideoRotationPlaybackTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
diff --git a/lab_system/master/config/tests/video_rotation_playback/video_rotation_playback/test_defs.json b/lab_system/master/config/tests/video_rotation_playback/video_rotation_playback/test_defs.json
new file mode 100644
index 0000000..c4205ce
--- /dev/null
+++ b/lab_system/master/config/tests/video_rotation_playback/video_rotation_playback/test_defs.json
@@ -0,0 +1,32 @@
+[
+  {
+    "group_name": "video_rotation_playback",
+    "slave_name": "video_rotation_playback",
+    "test_name": "video_rotation_playback",
+    "identifier": "video_rotation_playback",
+    "device_type": ["eureka", "lexx", "earth", "luther_b2"],
+    "device_group": "av_drm",
+    "branch": ["master", "release", "next_release"]
+  },
+  {
+    "group_name": "video_rotation_playback",
+    "slave_name": "video_rotation_playback",
+    "test_name": "video_rotation_playback",
+    "identifier": "video_rotation_playback_entourage",
+    "device_type": "dragonfly",
+    "device_group": "entourage",
+    "hidden": "true",
+    "branch": "cast_tv_dragonfly_master"
+  },
+  {
+    "group_name": "video_rotation_playback",
+    "slave_name": "video_rotation_playback",
+    "test_name": "video_rotation_playback",
+    "identifier": "video_rotation_playback_prod",
+    "device_type": ["eureka", "lexx"],
+    "device_group": "av_drm",
+    "branch": "on_demand",
+    "hidden": "true",
+    "scheduler": "0 3 * * *"
+  }
+]
diff --git a/lab_system/master/config/tests/volume/atv_volume.cfg b/lab_system/master/config/tests/volume/atv_volume.cfg
new file mode 100644
index 0000000..676fe96
--- /dev/null
+++ b/lab_system/master/config/tests/volume/atv_volume.cfg
@@ -0,0 +1,15 @@
+# Config file for Android TV volume tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.volume.atv_volume_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=VolumeTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.android_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=AndroidTestSequence
\ No newline at end of file
diff --git a/lab_system/master/config/tests/volume/fuchsia_volume.cfg b/lab_system/master/config/tests/volume/fuchsia_volume.cfg
new file mode 100644
index 0000000..2ec207c
--- /dev/null
+++ b/lab_system/master/config/tests/volume/fuchsia_volume.cfg
@@ -0,0 +1,17 @@
+# Config file for JavaScript video tests on Fuchsia devices.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.volume.volume_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=VolumeTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.fuchsia_javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=FuchsiaJavascriptTestSequence
+
+
diff --git a/lab_system/master/config/tests/volume/test_defs.json b/lab_system/master/config/tests/volume/test_defs.json
new file mode 100644
index 0000000..2373cf9
--- /dev/null
+++ b/lab_system/master/config/tests/volume/test_defs.json
@@ -0,0 +1,11 @@
+[
+  {
+    "group_name": "volume",
+    "slave_name": "volume",
+    "test_name": "volume",
+    "identifier": "volume_cast_tv",
+    "device_type": "dragonfly",
+    "device_group": "av_drm",
+    "branch": ["cast_tv_dragonfly_master", "cast_tv_dragonfly_release", "cast_tv_dragonfly_next_release"]
+  }
+]
diff --git a/lab_system/master/config/tests/volume/volume.cfg b/lab_system/master/config/tests/volume/volume.cfg
new file mode 100644
index 0000000..7ed8b1f
--- /dev/null
+++ b/lab_system/master/config/tests/volume/volume.cfg
@@ -0,0 +1,17 @@
+# Config file for JavaScript video tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.volume.volume_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=VolumeTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+
diff --git a/lab_system/master/config/tests/webgl_conformance/test_defs.json b/lab_system/master/config/tests/webgl_conformance/test_defs.json
new file mode 100644
index 0000000..367a9b2
--- /dev/null
+++ b/lab_system/master/config/tests/webgl_conformance/test_defs.json
@@ -0,0 +1,47 @@
+[
+  {
+    "group_name": "webgl_conformance",
+    "slave_name": "webgl_conformance",
+    "test_name": "webgl_conformance",
+    "identifier": "webgl_conformance",
+    "device_type": "eureka",
+    "device_group": "eureka_debug",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "webgl_conformance",
+    "slave_name": "webgl_conformance_v2",
+    "test_name": "webgl_conformance",
+    "identifier": "webgl_conformance_v2",
+    "device_type": "lexx",
+    "device_group": "lexx_eth",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "webgl_conformance",
+    "slave_name": "webgl_conformance_v3",
+    "test_name": "webgl_conformance",
+    "identifier": "webgl_conformance_v3",
+    "device_type": "earth",
+    "device_group": "earth_eth",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "webgl_conformance",
+    "slave_name": "webgl_conformance_v4",
+    "test_name": "webgl_conformance",
+    "identifier": "webgl_conformance_v4",
+    "device_type": "luther_b2",
+    "device_group": "luther_eth",
+    "branch": ["master", "release"]
+  },
+  {
+    "group_name": "webgl_conformance",
+    "slave_name": "webgl_conformance_v3",
+    "test_name": "webgl_conformance",
+    "identifier": "webgl_conformance_entourage",
+    "device_type": "dragonfly",
+    "device_group": "entourage",
+    "branch": "cast_tv_dragonfly_master"
+  }
+]
diff --git a/lab_system/master/config/tests/webgl_conformance/webgl_conformance.cfg b/lab_system/master/config/tests/webgl_conformance/webgl_conformance.cfg
new file mode 100644
index 0000000..356f47d
--- /dev/null
+++ b/lab_system/master/config/tests/webgl_conformance/webgl_conformance.cfg
@@ -0,0 +1,25 @@
+# Config file for the WebGL conformance tests.
+#
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.webgl_conformance.webgl_conformance_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=WebGLConformanceTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.framework.javascript_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=JavascriptTestSequence
+
+# Path in the media server (relative to continuous-tests/src) to the
+# WebGL conformance suite text file specifying tests to run
+WEBGL_CONFORMANCE_SUITE_PATH=tests/webgl-conformance-suites/1.0.1/00_test_list.txt
+
+# Version of WebGL conformance tests
+WEBGL_CONFORMANCE_VERSION=1.0.1
+
+# Comma-separated list of regular expressions to match the tests to skip.
+SKIPPED_TESTS=premultiplyalpha-test,gl-pixelstorei,tex-image-and-sub-image-2d-with-video,texture-npot-video
diff --git a/lab_system/master/config/tests/wifi_interface_connectivity/wifi_interface_connectivity.cfg b/lab_system/master/config/tests/wifi_interface_connectivity/wifi_interface_connectivity.cfg
new file mode 100644
index 0000000..88b47d2
--- /dev/null
+++ b/lab_system/master/config/tests/wifi_interface_connectivity/wifi_interface_connectivity.cfg
@@ -0,0 +1,13 @@
+# PROPERTIES
+#
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup_api_wifi.wifi_interface_connectivity_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=WifiInterfaceConnectivityTestCase,WifiInterfaceRebootTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.setup_api_wifi.wifi_interface_connectivity_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=WifiConnectivityTestSequence
diff --git a/lab_system/master/config/tests/wifi_multiconfig/wifi_multiconfig.cfg b/lab_system/master/config/tests/wifi_multiconfig/wifi_multiconfig.cfg
new file mode 100644
index 0000000..d16fa33
--- /dev/null
+++ b/lab_system/master/config/tests/wifi_multiconfig/wifi_multiconfig.cfg
@@ -0,0 +1,19 @@
+# PROPERTIES
+
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.wifi_multiconfig.wifi_multiconfig_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=WiFiMultiConfigTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.wifi_multiconfig.wifi_multiconfig_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=WiFiMultiConfigTestSequence
+
+# Numbers of random configurations to run
+ITERATIONS_COUNT=4
+
+
+
diff --git a/lab_system/master/config/tests/wifi_security_type/wifi_security_type.cfg b/lab_system/master/config/tests/wifi_security_type/wifi_security_type.cfg
new file mode 100644
index 0000000..0ee3847
--- /dev/null
+++ b/lab_system/master/config/tests/wifi_security_type/wifi_security_type.cfg
@@ -0,0 +1,13 @@
+# PROPERTIES
+#
+# Dot-notation path to the module containing the TestCase class:
+TEST_CASE_MODULE=eurtest.tests.setup_api_wifi.wifi_security_type_test_case
+
+# Name of the TestCase sub-class:
+TEST_CASE_CLASS=WifiSecurityTypeTestCase
+
+# Dot-notation path to the module containing the TestSequence class:
+TEST_SEQUENCE_MODULE=eurtest.tests.setup_api_wifi.wifi_interface_connectivity_test_sequence
+
+# Name of the TestSequence sub-class:
+TEST_SEQUENCE_CLASS=WifiConnectivityTestSequence
diff --git a/lab_system/update_fr.sh b/lab_system/update_fr.sh
new file mode 100755
index 0000000..2bfb5eb
--- /dev/null
+++ b/lab_system/update_fr.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+# Copyright 2013 Google Inc. All Rights Reserved.
+# Author: billiani@google.com (Leonardo Billiani)
+#
+# Script to update repositories and FR.
+# It assumes all the repositories are in the same root directory
+
+ACTION=$1
+SERVER=$2
+REPOSITORY_PATH=$3
+FR_DIRECTORY=$4
+FR_CONFIG_DIRECTORY=$5
+EXPECTED_ARGS=5
+
+function usage {
+    echo "Usage: $0 <restart|reconfig> <server> <repository_path> <fr_directory> <fr_config_directory>"
+    echo "    restart|reconfig    : 'restart' will restart the master and slaves."
+    echo "                          'reconfig' will update the master."
+    echo "                          Use reconfig when no changes to sequences have been made."
+    echo "    server              : Server name to update."
+    echo "    repository_path     : Root Location of all repositories to update."
+    echo "    fr_directory        : Path to fr installation"
+    echo "    fr_config_directory : Path to fr config directory"
+}
+
+if [ $# -lt $EXPECTED_ARGS ]
+then
+    usage
+    exit 0
+fi
+if [ $ACTION != 'restart' -a $ACTION != 'reconfig' ]
+then
+    usage
+    exit 0
+fi
+
+# Update repositories and do not output stdout
+source $HOME/.bashrc
+for repository in 'test' 'continuous-tests' 'autotest' 'fragglerock' 'chromite/chromite' 'chromium/src'
+do
+  cd $REPOSITORY_PATH/$repository && /usr/bin/git clean -f -d -x 1> /dev/null && /usr/bin/git fetch 1> /dev/null && /usr/bin/git reset --hard FETCH_HEAD 1> /dev/null
+done
+
+if [ $ACTION = 'restart' ]
+then
+  # Stop all the slaves to return all the devices
+  echo "Stopping the slaves"
+  curl http://$SERVER:8010/builders/_all/stopall
+  sleep 10
+fi
+cd $REPOSITORY_PATH/test && . ./setup_env.sh $FR_CONFIG_DIRECTORY
+PYTHONPATH=$PYTHONPATH:/usr/local/autotest
+cd $FR_DIRECTORY/buildbot/master && $FR_DIRECTORY/python-sandbox/bin/buildbot $ACTION
+if [ $ACTION = 'restart' ]
+then
+    # Start all the slaves after the restart
+    curl http://$SERVER:8010/builders/_all/forceall
+fi
diff --git a/lib/Makefile b/lib/Makefile
new file mode 100644
index 0000000..3f27d3e
--- /dev/null
+++ b/lib/Makefile
@@ -0,0 +1,48 @@
+# Makefile to create & manage python pip packages.
+#
+# Currently this only supports a single package but we can extend that similar
+# to https://eureka-internal.git.corp.google.com/fragglerock/+/master/Makefile
+#
+# Clean the generated files
+# $ make clean
+#
+# Create the pip package
+# $ make dist
+#
+# Deploy the package. Note you must be in the cast-test mdb and have valid loas
+# credentials to deploy.
+# $ make deploy
+#
+# Or just run make to deploy everything
+# $ make
+
+# The hostname where this file is being run.
+HOST=$(shell hostname)
+
+# The current folder.
+CWD=$(shell pwd)
+
+DEPLOYDIR=/cns/pf-d/home/cast-test/media/static/packages/
+PIP_TOOL=/google/data/ro/teams/chromecast/platform/tools/update_pip.par
+
+all: deploy
+
+clean:
+	rm -rf $(CWD)/dist
+	rm -rf $(CWD)/*.egg-info
+
+dist:
+	python setup.py sdist
+
+copy_package:
+	fileutil cp -f \
+		$(CWD)/dist/* \
+		$(DEPLOYDIR) \
+		--gfs_user cast-test
+
+update_repo:
+	$(PIP_TOOL) --nodry_run \
+		--package home-monitoring \
+		--environment prod
+
+deploy: dist copy_package update_repo clean
diff --git a/lib/README.md b/lib/README.md
new file mode 100644
index 0000000..4e628e1
--- /dev/null
+++ b/lib/README.md
@@ -0,0 +1,17 @@
+# Home Libraries
+
+A collection of shared libraries used across home infrastructure that can be
+deployed via pip packages.
+
+## Versioning
+
+Currently we have no set versioning scheme. If we make fundamentally breaking
+changes that would require a refactor we should update the minor version number.
+
+## Adding new libs
+
+1. Refactor the home dir into another sub dir - home-monitoring - and include
+  the setup.py file in that folders root
+2. Create the equivalent folder structure for the new lib
+3. Update the Makefile as described to include multiple packages
+4. Update these instructions
diff --git a/lib/__init__.py b/lib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/__init__.py
diff --git a/lib/home/__init__.py b/lib/home/__init__.py
new file mode 100644
index 0000000..a232304
--- /dev/null
+++ b/lib/home/__init__.py
@@ -0,0 +1,3 @@
+"""Declare namespace for shared packages."""
+
+__import__('pkg_resources').declare_namespace(__name__)
diff --git a/lib/home/monitoring/__init__.py b/lib/home/monitoring/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/home/monitoring/__init__.py
diff --git a/lib/home/monitoring/cloud_logging.py b/lib/home/monitoring/cloud_logging.py
new file mode 100644
index 0000000..2f20930
--- /dev/null
+++ b/lib/home/monitoring/cloud_logging.py
@@ -0,0 +1,640 @@
+"""Module for recording metrics and uploading to Google Cloud Logging.
+
+There are 3 methods of logging:
+1) Stackdriver / Cloud Logging API
+2) Local Logging
+3) Fluentd Agent
+
+Setup:
+  1) Stackdriver / Cloud Logging API, during application setup you should call:
+
+    project_name = 'google.com:cast-cq-staging'
+    logger_name = 'cast_cq.master'
+    cloud_logging.ConfigureLogger(project_name, logger_name)
+
+  2) Local Logging, this is used by default if you do not configure the cloud
+  logger or enable the fluentd logger
+
+  3) For Fluentd Agent, during application setup you should call:
+
+    namespace = "buildbot"
+    host = "127.0.0.0"
+    port = 24224
+    cloud_logging.EnableFluentdLogger(namespace, host, port)
+
+  There are few caveats when using the fluentd agent:
+  - The Google Cloud project will be inferred by the agent (either by explicitly
+    setting the project in the configuration or implicitly by the IAM Role)
+    that's how it determines which project to upload the logs to.
+  - Since we're using a version of the Google Cloud agent, it assumes that the
+    host it's running on is a GCE instance (which it may not). The consequence
+    of this is that the logs will be grouped into a GCE resource log like
+    "GCE VM Instance, <id>" and NOT "Global"
+  - Severity is ignored when using the agent
+  - Additional labels are also not passed to the agent
+
+Usage:
+  Then, throughout your application, you can use:
+
+  import cloud_logging
+
+  t = cloud_logging.Timer('test_timer').Start()
+  # do something interesting...
+  t.Stop()
+
+  with cloud_logging.Timer('test_timer') as t:
+    # do something interesting...
+    t.AddAttribute('a', 'b')
+
+  ev = cloud_logging.LogEvent('test_msg', value='Something to log').Log()
+  cloud_logging.Info('info_message', value='Something to log').Log()
+  cloud_logging.Error('error_message', value='Something to log').Log()
+"""
+import atexit
+import logging
+import os
+import socket
+import threading
+import time
+import traceback
+
+from fluent import event as fluentd_event
+from fluent import sender as fluentd  # TODO(b/70284159): switch to asyncsender
+
+from google.cloud import logging as gcl  # pylint: disable=ungrouped-imports
+from google.gax import errors
+from google.gax import grpc
+from google.protobuf import json_format
+import six
+
+# Subset of supported severity levels:
+# https://cloud.google.com/logging/docs/api/reference/rest/v2/LogEntry#LogSeverity
+DEBUG = 'DEBUG'
+INFO = 'INFO'
+WARNING = 'WARNING'
+ERROR = 'ERROR'
+
+SEVERITY_LEVELS = [DEBUG, INFO, WARNING, ERROR]
+# Global instances of configured cloud client/loggers
+_GLOBAL_CLIENT = None
+_GLOBAL_LOGGER = None
+_GLOBAL_THREAD = None
+_GLOBAL_LABELS = {}
+
+
+class BaseError(Exception):
+  """Base error."""
+
+
+class ConfigurationError(BaseError):
+  """Base error for invalid log configurations."""
+
+
+class EventError(BaseError):
+  """Base error for Metrics."""
+
+
+class TimerError(BaseError):
+  """Base error for Timers."""
+
+
+class StrictAttributesError(BaseError):
+  """Base Error for StrictAttributes decorator."""
+
+
+class LoggerError(BaseError):
+  """Base error for errors raised in logger."""
+
+  def __init__(self, original_error):
+    super(LoggerError, self).__init__(self)
+    self.original_error = original_error
+    self.original_trace = traceback.format_exc()
+
+  def __str__(self):
+    return self.original_trace
+
+
+class LoggerGaxError(LoggerError):
+  """Class for gax errors raised in logger."""
+
+
+class LogEvent(object):
+  """A high level LogEvent class used for logging events and data."""
+
+  def __init__(self, name, attributes=None, value=None, severity=None):
+    """Initialize a LogEvent.
+
+    Args:
+      name: The name of the event being timed.
+      attributes: A dict representing attributes to be recorded with this timer.
+      value: A value to include in the Metric.
+      severity: A string representing the log severity. One of:
+          'DEBUG', 'INFO', 'WARNING', 'ERROR' or None.
+    """
+    self._name = self._Sanitize(name)
+    self._attributes = {}
+    if not attributes:
+      attributes = {}
+    self.AddAttributes(attributes)
+    self._host = socket.getfqdn()
+    self._recorded_at = self._Now()
+    self._logged = False
+    self._value = self._Sanitize(value)
+    self._type = 'general'
+    self._severity = severity
+
+  @property
+  def type(self):
+    """Returns the type of event."""
+    return self._type
+
+  @property
+  def value(self):
+    """Returns the value for this event."""
+    return self._value
+
+  @value.setter
+  def value(self, value):
+    """Sets the value for this event."""
+    self._value = self._Sanitize(value)
+
+  @property
+  def severity(self):
+    """Returns the severity of event."""
+    return self._severity
+
+  @severity.setter
+  def severity(self, severity):
+    """Sets the severity of this event.
+
+    Only valid severity values will be accepted, all others will fail silently.
+
+    Args:
+      severity: A string representing the log severity. One of:
+          'DEBUG', 'INFO', 'WARNING', 'ERROR' or None.
+    """
+    self._severity = None
+    if severity in SEVERITY_LEVELS:
+      self._severity = severity
+
+  @property
+  def recorded_at(self):
+    """Get the timestamp for when this event was recorded."""
+    return self._recorded_at
+
+  @property
+  def name(self):
+    return self._name
+
+  @property
+  def host(self):
+    return self._host
+
+  @property
+  def attributes(self):
+    return self._attributes
+
+  def _Sanitize(self, value):
+    """Sanitizes arguments so they can be uploaded to GCL.
+
+    If it is a supported primitive that will be returned, otherwise its str
+    representation will be used.
+
+    This will also iterate through lists & dicts and sanitize their values.
+
+    Args:
+      value: The value to sanitize.
+
+    Returns:
+      The sanitized version of value.
+    """
+    if isinstance(value, (six.integer_types, float, bool)):
+      return value
+    if isinstance(value, list):
+      return [self._Sanitize(v) for v in value]
+    if isinstance(value, dict):
+      return {self._Sanitize(key): self._Sanitize(v)
+              for key, v in six.iteritems(value)}
+    try:
+      return str(value)
+    except Exception as e:  # pylint: disable=broad-except
+      return 'Could not sanitize object of type %s: %s' % (type(value), e)
+
+  def AddAttributes(self, attributes):
+    """Add more attributes to the event.
+
+    Args:
+      attributes: A dict of attributes to add to the current attribute list.
+                  They will override other attributes with the same keys.
+
+    Returns:
+      self.
+    """
+    if not attributes:
+      return self
+    for key, value in six.iteritems(attributes):
+      self.AddAttribute(key, value)
+    return self
+
+  def AddAttribute(self, name, value):
+    """Add a single attribute to the event.
+
+    Args:
+      name: The key to be used in the attribute dict.
+      value: The value to put for the given key. Note, if the key already
+             existed its value will be overridden.
+
+    Returns:
+      self.
+    """
+    self._attributes[self._Sanitize(name)] = self._Sanitize(value)
+    return self
+
+  def AsDict(self):
+    """Returns a dictionary representation of this event."""
+    result = {
+        'name': self.name,
+        'recorded_at_utc_usec': self.recorded_at,
+        'value': self.value,
+        'type': self.type,
+        'host': self.host,
+        'attributes': self.attributes
+    }
+    return result
+
+  def Valid(self):
+    """Returns if this event is valid."""
+    return True
+
+  def Log(self):
+    """Log this event.
+
+    Raises:
+      EventError: If the event is invalid.
+    """
+    if not self.Valid():
+      raise EventError('Event not valid.')
+    if not self._logged:
+      _SendEvent(self, severity=self._severity)
+      self._logged = True
+
+  def _Now(self):
+    """Returns the UTC usec timestamp for now."""
+    return time.time() * 1000 * 1000
+
+
+class Timer(LogEvent):
+  """A simpler timer object to record & log the duration of something."""
+
+  def __init__(self, name, attributes=None, severity=None):
+    """Initialize a timer.
+
+    Remember to call Start() to begin timing.
+
+    Args:
+      name: The name of the event being timed.
+      attributes: A dict representing attributes to be recorded with this timer.
+      severity: A string representing the log severity. One of:
+          'DEBUG', 'INFO', 'WARNING', 'ERROR' or None.
+    """
+    super(Timer, self).__init__(name, attributes=attributes, severity=severity)
+    self._started_at = None
+    self._finished_at = None
+    self._type = 'timer'
+
+  def Start(self):
+    """Starts the timer.
+
+    Returns:
+      self.
+    """
+    self._started_at = self._Now()
+    return self
+
+  def Stop(self):
+    """Stops the timer and records the result.
+
+    Note that the timer must have been started.
+
+    Returns:
+      self.
+
+    Raises:
+      TimerError: If the timer has not yet been started.
+    """
+    if not self.started:
+      raise TimerError('Timer not yet started.')
+    self._finished_at = self._Now()
+    self.Log()
+    return self
+
+  @property
+  def started(self):
+    """Returns boolean if this timer has been started."""
+    return self._started_at is not None
+
+  @property
+  def finished(self):
+    """Returns boolean if this timer has been completed."""
+    return self._finished_at is not None
+
+  def Time(self):
+    """Returns the value recorded by this timer.
+
+    Returns:
+      The timed value.
+
+    Raises:
+      TimerError: If the timer has not been started or completed.
+    """
+    if not self.started:
+      raise TimerError('Timer not yet started.')
+    if not self.finished:
+      raise TimerError('Timer not yet finished.')
+    return self._finished_at - self._started_at
+
+  @property
+  def value(self):
+    return self.Time()
+
+  @property
+  def recorded_at(self):
+    return self._finished_at
+
+  def Valid(self):
+    try:
+      self.Time()
+      return True
+    except TimerError:
+      return False
+
+  def __enter__(self):
+    self.Start()
+    return self
+
+  def __exit__(self, *args):
+    self.Stop()
+
+
+class StrictAttributes(object):
+  """Decorator for LogEvents that require a fixed set of attributes.
+
+  This decorator should be used when collecting metrics to be uploaded to
+  BigQuery. All attributes to be collected must be included in valid_attributes,
+  mapped to their default values. Any valid_attributes not added before Log()
+  will be included with the specified default value.
+
+  Sample Usage:
+
+  VALID_ATTRIBUTES = {
+      'status': StrictAttributes.DEFAULT_STRING,
+      'count': StrictAttributes.DEFAULT_INT }
+
+  @StrictAttributes(VALID_ATTRIBUTES)
+  class MyLogger(Timer):
+    ...
+  """
+  # TODO(adrexler): Migrate valid_attributes to protobuf.
+
+  # Default values for all accepted attribute datatypes.
+  DEFAULT_STRING = ''
+  DEFAULT_BOOL = False
+  DEFAULT_LIST = []
+  DEFAULT_DICT = {}
+  DEFAULT_INT = -1
+  DEFAULT_FLOAT = float(DEFAULT_INT)
+  try:
+    DEFAULT_LONG = long(DEFAULT_INT)
+  except NameError:
+    DEFAULT_LONG = int(DEFAULT_INT)  # pylint: disable=redefined-variable-type
+
+  def __init__(self, valid_attributes):
+    """Constructor.
+
+    Args:
+      valid_attributes: All acceptable attributes, mapped to default values.
+    """
+    self._valid_attributes = valid_attributes
+
+  def __call__(self, cls, *args, **kwargs):
+    """Returns MetricsCollector class, extending cls."""
+    if not issubclass(cls, LogEvent):
+      raise StrictAttributesError('%s does not extend LogEvent!', cls)
+    valid_attributes = self._valid_attributes
+
+    class MetricsCollector(cls):
+      """Extension of LogEvent child cls, with strict attributes."""
+
+      def __init__(self, *args, **kwargs):
+        self._valid_attributes = valid_attributes
+        super(MetricsCollector, self).__init__(*args, **kwargs)
+
+      def AddAttribute(self, name, value):
+        """Adds value for attribute after checking that attribute is valid."""
+        if name not in self._valid_attributes:
+          logging.error('%s is not a valid attribute!', name)
+          return
+        super(MetricsCollector, self).AddAttribute(name, value)
+
+      def Log(self):
+        """Adds specified default values for missing attributes and logs."""
+        for attribute in self._valid_attributes:
+          if attribute not in self._attributes:
+            self.AddAttribute(attribute, self._valid_attributes[attribute])
+        super(MetricsCollector, self).Log()
+    return MetricsCollector
+
+
+class BatchThread(threading.Thread):
+  """A simple thread worker to call commit on the Batch Logger."""
+
+  def __init__(self, duration, err_limit=3, sleep=2.0):
+    super(BatchThread, self).__init__()
+    self._duration = duration
+    self._sleep = sleep
+    self._stop = threading.Event()
+    self._err_limit = err_limit
+
+  def run(self):
+    while not self._stop.isSet():
+      time.sleep(self._duration)
+      if not _CommitWithRetry(self._sleep, self._err_limit):
+        logging.error('Terminated cloud logging due to non-transient error')
+        self._stop.set()
+
+  def join(self, timeout=None):
+    self._stop.set()
+    super(BatchThread, self).join(timeout)
+
+
+def Debug(*args, **kwargs):
+  return LogEvent(*args, severity=DEBUG, **kwargs)
+
+
+def Info(*args, **kwargs):
+  return LogEvent(*args, severity=INFO, **kwargs)
+
+
+def Warn(*args, **kwargs):
+  return LogEvent(*args, severity=WARNING, **kwargs)
+
+
+def Error(*args, **kwargs):
+  return LogEvent(*args, severity=ERROR, **kwargs)
+
+
+def _SendEvent(event, severity=None):
+  """Log a metric.
+
+  Args:
+    event: A LogEvent to log. Must have an AsDict method implemented.
+    severity: A string representing the log severity. This will be ignored if
+              fluentd is enabled.
+
+  Raises:
+    LoggerGaxError: If GaxError other then DEADLINE_EXCEEDED was
+                    raised during logger call.
+    LoggerError: If any error other then ParseError or GaxError was
+                 raised during logger call.
+  """
+  if fluentd.get_global_sender():
+    fluentd_event.Event(None, event.AsDict())
+  elif _GLOBAL_LOGGER:
+    try:
+      _GLOBAL_LOGGER.log_struct(
+          event.AsDict(), labels=_GLOBAL_LABELS, severity=severity)
+    except errors.GaxError as e:
+      if grpc.exc_to_code(e.cause) != grpc.StatusCode.DEADLINE_EXCEEDED:
+        raise LoggerGaxError(e)
+      logging.exception('Gax RPC Deadline exceeded while push to log event %s'
+                        ' with labels %s, not expecting event loss in cloud',
+                        event.AsDict(), _GLOBAL_LABELS)
+    except json_format.ParseError as e:
+      logging.exception('Unable to log event %s with labels %s'
+                        ' event will not be reported to cloud',
+                        event.AsDict(), _GLOBAL_LABELS)
+    except Exception as e:  # pylint: disable=broad-except
+      raise LoggerError(e)
+  else:
+    logging.info(event.AsDict())
+
+
+def _CommitWithRetry(sleep=2.0, retries=3):
+  """Commiting event queue. Dispatching errors and recover if possible.
+
+  The only transient error we are aware of is a Gax RetryError.
+  In 99% cases we succeed from 3rd commit and will not succeed after that
+  without clearing commit queue.
+  For other then Gax Retry Error we will not recover, so returning False
+
+  Args:
+    sleep: Sleep period between retries in case of error.
+    retries: Maximum number of commits before clearing queue.
+  Return:
+    Boolean if the batch process should continue
+  """
+  while retries > 0:
+    try:
+      _GLOBAL_LOGGER.commit()
+      return True
+    except errors.RetryError as e:
+      retries -= 1
+      queue_size = len(_GLOBAL_LOGGER.entries[:])
+      err = str(e)
+      logging.exception('Gax Retry Error while commit log buffer,'
+                        ' %d retries left,%s %d events in queue', retries,
+                        '' if retries else ' dropping', queue_size)
+      if retries > 0:
+        Warn('RetryError', value=queue_size, attributes={
+             'retries_left': retries, 'error': err,
+             'dropping_queue': False}).Log()
+        time.sleep(sleep)
+      else:
+        del _GLOBAL_LOGGER.entries[:]
+        Warn('RetryError', value=queue_size, attributes={
+             'retries_left': 0, 'error': err,
+             'dropping_queue': True}).Log()
+    except errors.GaxError as e:
+      logging.exception('Gax Error while commit log buffer, stopping batch')
+      return False
+    except Exception as e:  # pylint: disable=broad-except
+      logging.exception('Unexpected Error while commit log buffer,'
+                        ' stopping batch')
+      return False
+  return True
+
+def ConfigureLogger(project_name, logger_name, labels=None, batch_time=None,
+                    batch_err_limit=3):
+  """Configure a Cloud Logger.
+
+  Does NOT enable logging to a fluentd agent. See EnableFluentdLogger()
+
+  Args:
+    project_name: The name of the project to log to.
+    logger_name: The name of the logger.
+    labels: An optional dict of labels to apply to each message.
+    batch_time: use a batch logger to send logs every n seconds.
+    batch_err_limit: max consecutive errors before batch logging is terminated.
+
+  Raises:
+    ConfigurationError: If no credentials were found.
+  """
+  if 'GOOGLE_APPLICATION_CREDENTIALS' not in os.environ:
+    raise ConfigurationError(
+        ('No credentials found. Make sure a json based key is available in the '
+         'GOOGLE_APPLICATION_CREDENTIALS env var.')
+    )
+
+  global _GLOBAL_CLIENT
+  global _GLOBAL_LOGGER
+  global _GLOBAL_THREAD
+  _GLOBAL_CLIENT = gcl.Client(project=project_name)
+  _GLOBAL_LOGGER = _GLOBAL_CLIENT.logger(logger_name)
+  if batch_time:
+    _GLOBAL_LOGGER = _GLOBAL_LOGGER.batch()
+    _GLOBAL_THREAD = BatchThread(batch_time, batch_err_limit)
+    _GLOBAL_THREAD.start()
+  _GLOBAL_LABELS['host'] = socket.getfqdn()
+  if labels:
+    _GLOBAL_LABELS.update(labels)
+
+def SafeConfigureLogger(*args, **kwargs):
+  """Configures a Cloud Logger, if possible. Otherwise fails silently.
+
+     Does NOT enable logging to a fluentd agent. See EnableFluentdLogger()
+  """
+  try:
+    ConfigureLogger(*args, **kwargs)
+  except ConfigurationError:
+    logging.error('Unable to configure cloud logging: No GAE credentials '
+                  'found. Logging to console only.')
+
+def EnableFluentdLogger(namespace, host="127.0.0.1", port=24224):
+  """Enable logging to a fluentd agent for logging to Google Cloud.
+
+  This disables any other form of logging, including both the Cloud Logging API
+  and local logging.
+
+  Args:
+    namespace: The namespace for logs in this application. For example
+               "buildbot", which becomes referenced in Google Cloud as
+               logName: "projects/<project>/logs/<namespace>"
+    host: The hostname or IP the fluentd agent lives on.
+    port: The port the fluentd agent is listening on.
+  """
+  if fluentd.get_global_sender() is None:
+    fluentd.setup(namespace, host=host, port=port)
+
+@atexit.register
+def StopLogger():
+  """
+    Stop the batch logger thread and/or fluentd logger if started. No more
+    logs will be sent.
+
+    This will be executed on application termination
+  """
+  if _GLOBAL_THREAD:
+    _GLOBAL_THREAD.join()
+
+  if fluentd.get_global_sender():
+    fluentd.close()
diff --git a/lib/home/monitoring/cloud_logging_test.py b/lib/home/monitoring/cloud_logging_test.py
new file mode 100644
index 0000000..5185755
--- /dev/null
+++ b/lib/home/monitoring/cloud_logging_test.py
@@ -0,0 +1,51 @@
+"""Tests for cloud_logging."""
+
+from __future__ import absolute_import
+import unittest
+from . import cloud_logging
+
+try:
+  from unittest import mock  # pylint: disable=wrong-import-order
+except ImportError:
+  import mock
+
+class StrictAttributesTest(unittest.TestCase):
+
+  def setUp(self):
+    self.valid_attributes = {
+        'status': cloud_logging.StrictAttributes.DEFAULT_STRING,
+        'count': cloud_logging.StrictAttributes.DEFAULT_INT,
+    }
+
+    @cloud_logging.StrictAttributes(self.valid_attributes)
+    class Collector(cloud_logging.LogEvent):
+      """Extends LogEvent."""
+
+    self.collector = Collector('test_logger')
+
+  def testVerifySubclass(self):
+    """Verify that output class is a subclass of LogEvent."""
+    self.assertIsInstance(self.collector, cloud_logging.LogEvent)
+
+  def testAddAttribute(self):
+    """Add a valid attribute to the dict of _attributes."""
+    self.collector.AddAttribute('status', 'SUCCESS')
+
+    self.assertIn('status', self.collector._attributes)
+    self.assertEqual(self.collector._attributes['status'], 'SUCCESS')
+
+  def testAddInvalidAttribute(self):
+    """Attempt to add an invalid attribute."""
+    self.collector.AddAttribute('invalid', True)
+    self.assertNotIn('invalid', self.collector._attributes)
+
+  @mock.patch.object(cloud_logging.LogEvent, 'Log')
+  def testLog(self, mock_log):
+    self.collector.Log()
+    mock_log.assert_called_once()
+    for attribute in self.valid_attributes:
+      self.assertIn(attribute, self.collector._attributes)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/lib/setup.py b/lib/setup.py
new file mode 100644
index 0000000..8700d8f
--- /dev/null
+++ b/lib/setup.py
@@ -0,0 +1,15 @@
+"""Setup script for home based libraries."""
+
+from __future__ import absolute_import
+from setuptools import find_packages
+from setuptools import setup
+
+setup(
+    name='home-monitoring',
+    version='0.15.dev',
+    packages=find_packages(),
+    install_requires=['google-cloud-logging'],
+    namespace_packages=['home'],
+    maintainer='Home EngProd',
+    maintainer_email='home-engprod-infra@google.com'
+)
diff --git a/pylintrc b/pylintrc
new file mode 100644
index 0000000..942d8ff
--- /dev/null
+++ b/pylintrc
@@ -0,0 +1,345 @@
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+ignore-patterns=.*_pb2.py  # Ignore proto genfiles.
+
+# Pickle collected data for later comparisons.
+persistent=no
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time.
+#enable=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once).
+#
+#
+# These were disabled specificially for continuous-tests/:
+#     attribute-defined-outside-init,
+#     bad-option-value,
+#     method-hidden,
+#
+# These were inherited from depot_tools and should be audited, to see if
+# continuous-tests/ could benefit from them:
+#     anomalous-backslash-in-string
+#     bad-context-manager
+#     bad-continuation
+#     bad-indentation
+#     bad-str-strip-call
+#     bad-whitespace
+#     cell-var-from-loop
+#     deprecated-lambda
+#     eval-used
+#     function-redefined
+#     import-error
+#     locally-enabled
+#     missing-final-newline
+#     no-init
+#     no-name-in-module
+#     no-self-use
+#     not-callable
+#     old-style-class
+#     protected-access
+#     super-on-old-class
+#     superfluous-parens
+#     too-many-function-args
+#     trailing-whitespace
+#     unnecessary-semicolon
+#     unpacking-non-sequence
+#     unused-import
+#     useless-else-on-loop
+#
+disable=
+    abstract-class-little-used,
+    abstract-class-not-used,
+    anomalous-backslash-in-string,
+    attribute-defined-outside-init,
+    bad-builtin,
+    bad-context-manager,
+    bad-continuation,
+    bad-indentation,
+    bad-inline-option,
+    bad-option-value,
+    bad-str-strip-call,
+    bad-whitespace,
+    broad-except,
+    cell-var-from-loop,
+    deprecated-lambda,
+    deprecated-module,
+    duplicate-code,
+    eval-used,
+    exec-used,
+    fixme,
+    function-redefined,
+    global-statement,
+    import-error,
+    invalid-name,
+    locally-disabled,
+    locally-enabled,
+    logging-not-lazy,
+    method-hidden,
+    missing-docstring,
+    missing-final-newline,
+    no-init,
+    no-name-in-module,
+    no-self-use,
+    not-callable,
+    old-style-class,
+    protected-access,
+    reimported,
+    star-args,
+    super-on-old-class,
+    superfluous-parens,
+    too-few-public-methods,
+    too-many-ancestors,
+    too-many-arguments,
+    too-many-branches,
+    too-many-function-args,
+    too-many-instance-attributes,
+    too-many-lines,
+    too-many-locals,
+    too-many-public-methods,
+    too-many-return-statements,
+    too-many-statements,
+    trailing-whitespace,
+    unnecessary-semicolon,
+    unpacking-non-sequence,
+    unused-import,
+    useless-else-on-loop,
+    # Disabled while we run pylint 2 against the code.
+    # these should be disabled after upgrade to Python 3
+    # is complete.
+    useless-object-inheritance
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=text
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells whether to display a full report or only the messages
+# CHANGED:
+reports=no
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (RP0004).
+comment=no
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the beginning of the name of dummy variables
+# (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+ignored-classes=SQLObject,twisted.internet.reactor,hashlib,google.appengine.api.memcache
+
+# When zope mode is activated, add a predefined set of Zope acquired attributes
+# to generated-members.
+zope=no
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E0201 when accessed. Python regular
+# expressions are accepted.
+generated-members=REQUEST,acl_users,aq_parent,multiprocessing.managers.SyncManager
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+# CHANGED:
+indent-string='  '
+
+
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,apply,input,reduce
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=((_?[A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression which should only match correct function names
+function-rgx=^(?:(?P<camel_case>_?[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_?[a-z][a-z0-9_]*))$
+
+# Regular expression which should only match correct method names
+method-rgx=^(?:(?P<exempt>__[a-z0-9_]+__|next)|(?P<camel_case>_{0,2}[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_{0,2}[a-z][a-z0-9_]*)|(?P<testcsae>test[A-Z][a-zA-Z0-9_]*))$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=^_{0,2}[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=^[a-z][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=^[a-z_][a-z0-9_]*$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,main,Run,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=__.*__
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branchs=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=StandardError,Exception,BaseException
diff --git a/test_data/README b/test_data/README
new file mode 100644
index 0000000..d81f35e
--- /dev/null
+++ b/test_data/README
@@ -0,0 +1,2 @@
+Test data used by unit tests in the test/ repo that
+cannot be shared with partners.
diff --git a/test_data/androidthings/dev.keystore b/test_data/androidthings/dev.keystore
new file mode 100644
index 0000000..45bb740
--- /dev/null
+++ b/test_data/androidthings/dev.keystore
Binary files differ
diff --git a/test_data/assistant_audios/audio_metadata_format.md b/test_data/assistant_audios/audio_metadata_format.md
new file mode 100644
index 0000000..454e712
--- /dev/null
+++ b/test_data/assistant_audios/audio_metadata_format.md
@@ -0,0 +1,29 @@
+# Assistant Audio Metadata File Format.
+
+Each audio metadata file is used to describe the recorded audio samples. These
+metadata files should be of json format.
+
+## Allowed field names and value formats
+
+The following field names and corresponding value formats are allowed in the
+metadata file:
+
+* audio_file_path - A string; This should be the absolute path/uri of the
+  recorded audio sample.
+* script - A string; This should be the text used to record the audio sample.
+  eg: Who is obama?
+* number_of_channels - An Integer; The number of channels, the recorded audio
+  sample has.
+* sample_format -  A string; The sample format of recorded stream container.
+  eg: S32LE
+* sample_rate - An Integer; The sampling rate of recorded audio sample in hertz.
+
+## Sample File
+
+  {
+    "audio_file_path": "okgooglesetalarmat5am.raw",
+    "script": "set alarm at 5 a.m.",
+    "number_of_channels": "8",
+    "sample_format": "S32LE",
+    "sample_rate": "48000"
+  }
\ No newline at end of file
diff --git a/test_data/assistant_audios/hotword_effort_level_high.json b/test_data/assistant_audios/hotword_effort_level_high.json
new file mode 100644
index 0000000..9b415ee
--- /dev/null
+++ b/test_data/assistant_audios/hotword_effort_level_high.json
@@ -0,0 +1,8 @@
+{
+  "audio_file_path": "hotword_effort_level_high.wav",
+  "script": "what's the time",
+  "number_of_channels": "2",
+  "sample_format": "InterleavedInt32",
+  "sample_rate": "16000"
+}
+
diff --git a/test_data/assistant_audios/hotword_effort_level_high.wav b/test_data/assistant_audios/hotword_effort_level_high.wav
new file mode 100644
index 0000000..b55dccd
--- /dev/null
+++ b/test_data/assistant_audios/hotword_effort_level_high.wav
Binary files differ
diff --git a/test_data/assistant_audios/hotword_effort_level_low.json b/test_data/assistant_audios/hotword_effort_level_low.json
new file mode 100644
index 0000000..fcb18d5
--- /dev/null
+++ b/test_data/assistant_audios/hotword_effort_level_low.json
@@ -0,0 +1,8 @@
+{
+  "audio_file_path": "hotword_effort_level_low.wav",
+  "script": "what's the time",
+  "number_of_channels": "2",
+  "sample_format": "InterleavedInt32",
+  "sample_rate": "16000"
+}
+
diff --git a/test_data/assistant_audios/hotword_effort_level_low.wav b/test_data/assistant_audios/hotword_effort_level_low.wav
new file mode 100644
index 0000000..e081dd3
--- /dev/null
+++ b/test_data/assistant_audios/hotword_effort_level_low.wav
Binary files differ
diff --git a/test_data/assistant_audios/hotword_effort_level_medium.json b/test_data/assistant_audios/hotword_effort_level_medium.json
new file mode 100644
index 0000000..3a03add
--- /dev/null
+++ b/test_data/assistant_audios/hotword_effort_level_medium.json
@@ -0,0 +1,8 @@
+{
+  "audio_file_path": "hotword_effort_level_medium.wav",
+  "script": "what's the time",
+  "number_of_channels": "2",
+  "sample_format": "InterleavedInt32",
+  "sample_rate": "16000"
+}
+
diff --git a/test_data/assistant_audios/hotword_effort_level_medium.wav b/test_data/assistant_audios/hotword_effort_level_medium.wav
new file mode 100644
index 0000000..dcf446e
--- /dev/null
+++ b/test_data/assistant_audios/hotword_effort_level_medium.wav
Binary files differ
diff --git a/test_data/assistant_audios/okgoogleplayladygaga.wav b/test_data/assistant_audios/okgoogleplayladygaga.wav
new file mode 100644
index 0000000..86c1e9e
--- /dev/null
+++ b/test_data/assistant_audios/okgoogleplayladygaga.wav
Binary files differ
diff --git a/test_data/assistant_audios/okgooglesetalarmat5am.wav b/test_data/assistant_audios/okgooglesetalarmat5am.wav
new file mode 100644
index 0000000..9c166cf
--- /dev/null
+++ b/test_data/assistant_audios/okgooglesetalarmat5am.wav
Binary files differ
diff --git a/test_data/assistant_audios/okgooglesettimerfor5seconds.wav b/test_data/assistant_audios/okgooglesettimerfor5seconds.wav
new file mode 100644
index 0000000..251e931
--- /dev/null
+++ b/test_data/assistant_audios/okgooglesettimerfor5seconds.wav
Binary files differ
diff --git a/test_data/assistant_audios/okgooglewhatistheweather.wav b/test_data/assistant_audios/okgooglewhatistheweather.wav
new file mode 100644
index 0000000..99f54e1
--- /dev/null
+++ b/test_data/assistant_audios/okgooglewhatistheweather.wav
Binary files differ
diff --git a/test_data/assistant_audios/okgooglewhattimeisit.wav b/test_data/assistant_audios/okgooglewhattimeisit.wav
new file mode 100644
index 0000000..88a4af5
--- /dev/null
+++ b/test_data/assistant_audios/okgooglewhattimeisit.wav
Binary files differ
diff --git a/test_data/assistant_audios/play_lady_gaga.json b/test_data/assistant_audios/play_lady_gaga.json
new file mode 100644
index 0000000..b6d7d1f
--- /dev/null
+++ b/test_data/assistant_audios/play_lady_gaga.json
@@ -0,0 +1,8 @@
+{
+  "audio_file_path": "okgoogleplayladygaga.wav",
+  "script": "play lady gaga",
+  "number_of_channels": "2",
+  "sample_format": "InterleavedInt32",
+  "sample_rate": "16000"
+}
+
diff --git a/test_data/assistant_audios/set_alarm.json b/test_data/assistant_audios/set_alarm.json
new file mode 100644
index 0000000..05f89a4
--- /dev/null
+++ b/test_data/assistant_audios/set_alarm.json
@@ -0,0 +1,8 @@
+{
+  "audio_file_path": "okgooglesetalarmat5am.wav",
+  "script": "set alarm at 5 a.m.",
+  "number_of_channels": "2",
+  "sample_format": "InterleavedInt32",
+  "sample_rate": "16000"
+}
+
diff --git a/test_data/assistant_audios/set_timer.json b/test_data/assistant_audios/set_timer.json
new file mode 100644
index 0000000..93609fe
--- /dev/null
+++ b/test_data/assistant_audios/set_timer.json
@@ -0,0 +1,9 @@
+{
+  "audio_file_path": "okgooglesettimerfor5seconds.wav",
+  "script": "set timer for 5 seconds",
+  "number_of_channels": "2",
+  "sample_format": "InterleavedInt32",
+  "sample_rate": "16000"
+  
+}
+
diff --git a/test_data/assistant_audios/what_is_the_weather.json b/test_data/assistant_audios/what_is_the_weather.json
new file mode 100644
index 0000000..b2aa1b8
--- /dev/null
+++ b/test_data/assistant_audios/what_is_the_weather.json
@@ -0,0 +1,8 @@
+{
+  "audio_file_path": "okgooglewhatistheweather.wav",
+  "script": "what's the weather in San Francisco",
+  "number_of_channels": "2",
+  "sample_format": "InterleavedInt32",
+  "sample_rate": "16000"
+}
+
diff --git a/test_data/assistant_audios/what_time_is_it.json b/test_data/assistant_audios/what_time_is_it.json
new file mode 100644
index 0000000..9446818
--- /dev/null
+++ b/test_data/assistant_audios/what_time_is_it.json
@@ -0,0 +1,8 @@
+{
+  "audio_file_path": "okgooglewhattimeisit.wav",
+  "script": "what time is it",
+  "number_of_channels": "2",
+  "sample_format": "InterleavedInt32",
+  "sample_rate": "16000"
+}
+
diff --git a/test_data/cast_ota_installer/README b/test_data/cast_ota_installer/README
new file mode 100644
index 0000000..51579dc
--- /dev/null
+++ b/test_data/cast_ota_installer/README
@@ -0,0 +1,3 @@
+Test files for eurtest.tests.cast_audio.cast_ota_installer tests.
+
+Signed OTA packaged obtained from the master/26752 build.
diff --git a/test_data/cast_ota_installer/cast_receiver_signed-test_private_key.zip b/test_data/cast_ota_installer/cast_receiver_signed-test_private_key.zip
new file mode 100644
index 0000000..33ed1b3
--- /dev/null
+++ b/test_data/cast_ota_installer/cast_receiver_signed-test_private_key.zip
Binary files differ
diff --git a/test_data/cast_ota_installer/cast_receiver_signed.zip b/test_data/cast_ota_installer/cast_receiver_signed.zip
new file mode 100644
index 0000000..c9d31e1
--- /dev/null
+++ b/test_data/cast_ota_installer/cast_receiver_signed.zip
Binary files differ
diff --git a/test_data/cast_ota_installer/cast_receiver_unsigned.zip b/test_data/cast_ota_installer/cast_receiver_unsigned.zip
new file mode 100644
index 0000000..2572b6e
--- /dev/null
+++ b/test_data/cast_ota_installer/cast_receiver_unsigned.zip
Binary files differ
diff --git a/test_data/test_audio_samples/empty_sequence.wav b/test_data/test_audio_samples/empty_sequence.wav
new file mode 100644
index 0000000..878bf23
--- /dev/null
+++ b/test_data/test_audio_samples/empty_sequence.wav
Binary files differ
diff --git a/test_data/test_audio_samples/fine_aligner_ref.wav b/test_data/test_audio_samples/fine_aligner_ref.wav
new file mode 100644
index 0000000..a8eeb98
--- /dev/null
+++ b/test_data/test_audio_samples/fine_aligner_ref.wav
Binary files differ
diff --git a/test_data/test_audio_samples/fine_aligner_src.wav b/test_data/test_audio_samples/fine_aligner_src.wav
new file mode 100644
index 0000000..a273758
--- /dev/null
+++ b/test_data/test_audio_samples/fine_aligner_src.wav
Binary files differ
diff --git a/test_data/test_audio_samples/sign_check_ref.wav b/test_data/test_audio_samples/sign_check_ref.wav
new file mode 100644
index 0000000..a5803db
--- /dev/null
+++ b/test_data/test_audio_samples/sign_check_ref.wav
Binary files differ
diff --git a/test_data/test_audio_samples/sign_check_src.wav b/test_data/test_audio_samples/sign_check_src.wav
new file mode 100644
index 0000000..f0d0166
--- /dev/null
+++ b/test_data/test_audio_samples/sign_check_src.wav
Binary files differ
diff --git a/test_data/test_audio_samples/sunshine-reference-test.wav b/test_data/test_audio_samples/sunshine-reference-test.wav
new file mode 100644
index 0000000..49a41e9
--- /dev/null
+++ b/test_data/test_audio_samples/sunshine-reference-test.wav
Binary files differ
diff --git a/test_data/test_audio_samples/sunshine-reference.wav b/test_data/test_audio_samples/sunshine-reference.wav
new file mode 100644
index 0000000..a885951
--- /dev/null
+++ b/test_data/test_audio_samples/sunshine-reference.wav
Binary files differ
diff --git a/test_data/test_audio_samples/sunshine-v2.wav b/test_data/test_audio_samples/sunshine-v2.wav
new file mode 100644
index 0000000..66ae364
--- /dev/null
+++ b/test_data/test_audio_samples/sunshine-v2.wav
Binary files differ
diff --git a/test_data/test_audio_samples/sunshine_record.wav b/test_data/test_audio_samples/sunshine_record.wav
new file mode 100644
index 0000000..8d259d2
--- /dev/null
+++ b/test_data/test_audio_samples/sunshine_record.wav
Binary files differ
diff --git a/test_data/test_audio_samples/synthesized-sequence.wav b/test_data/test_audio_samples/synthesized-sequence.wav
new file mode 100644
index 0000000..2d57396
--- /dev/null
+++ b/test_data/test_audio_samples/synthesized-sequence.wav
Binary files differ
diff --git a/test_data/test_audio_samples/test_sequence.wav b/test_data/test_audio_samples/test_sequence.wav
new file mode 100644
index 0000000..9bc13fb
--- /dev/null
+++ b/test_data/test_audio_samples/test_sequence.wav
Binary files differ