Scripts to upload CTS-M/CTS-V results to buckets.

BUG=b:62536312
TEST=./cts_upload.py <build_id> <results_dir_path>
TEST=./cts_upload_verifier.py <build_id> <results_dir_path>

Change-Id: I08a1806d4b2395469261545f7b92d30759d50850
Reviewed-on: https://chromium-review.googlesource.com/658321
Commit-Ready: Nita Nair <nnita@chromium.org>
Tested-by: Nita Nair <nnita@chromium.org>
Reviewed-by: Rohit Makasana <rohitbm@chromium.org>
diff --git a/provingground/cts/lib/__init__.py b/provingground/cts/lib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/provingground/cts/lib/__init__.py
diff --git a/provingground/cts/lib/upload_utils.py b/provingground/cts/lib/upload_utils.py
new file mode 100755
index 0000000..23c911e
--- /dev/null
+++ b/provingground/cts/lib/upload_utils.py
@@ -0,0 +1,298 @@
+#!/usr/bin/env python3
+#
+# Copyright 2017 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This script has helper methods to facilitate CTS/CTS-V upload."""
+
+from __future__ import print_function
+from subprocess import check_call
+
+import os
+import shutil
+import zipfile
+
+
+class UploadUtils(object):
+  """Internal Helper for CTS and CTS Verifier Upload scripts."""
+
+  SPACE_COUNT_CTSV_BOARD_NAME = 14
+  SPACE_COUNT_CTSV_BUILD_VERSION = 22
+  SPACE_COUNT_VERIFIER_VERSION = 18
+  SPACE_COUNT_TIMESTAMP = 17
+  SPACE_COUNT_CTS_BOARD_NAME = 13
+  SPACE_COUNT_CTS_BUILD_VERSION = 12
+  SPACE_COUNT_CTS_BOARD_ABI = 13
+  SPACE_COUNT_PACKAGE_NAME = 30
+  SPACE_COUNT_FAILED_TESTS = 6
+  SPACE_COUNT_PASSED_TESTS = 6
+  C_PURPLE = '\033[95m'
+  C_RED = '\033[91m'
+  C_BLACK = '\033[98m'
+  BOLD = '\033[1m'
+  C_END = '\033[00m'
+  TABLE_HEADER_CTS = (' Time Stamp           : Board   :'
+                      'Build Version : Board ABI : '
+                      'TEST PACKAGE / MODULE NAME  : FAILED :'
+                      ' PASSED')
+  TABLE_HEADER_CTSV = (' Build Board Name : '
+                       'Build Board Version : '
+                       'CTS VERIFIER VERSION')
+  def __init__(self):
+    self.build_info_list = []
+    self.obsolete_file_count = 0
+    self.untested_file_count = 0
+    self.build_mismatch_file_count = 0
+
+  def PrintObsoleteFolderCount(self):
+    """Prints obsolete file count in case of build id mismatch."""
+    if not self.build_mismatch_file_count == 0:
+      err_msg = ('Build ID Mismatch!Pushed {0} invalid files to BuildID'
+                 ' Mismatch Folder'.format(self.build_mismatch_file_count))
+      print(self.BOLD + self.C_RED + err_msg +self.BOLD+ self.C_END)
+
+  def PrintUntestedFolderCount(self):
+    """Prints untested file count if untested tests exist."""
+    if not self.untested_file_count == 0:
+      err_msg = ('Untested Test(s) Detected!Pushed {0} files to Untested'
+                 ' Tests Folder'.format(self.untested_file_count))
+      print(self.BOLD + self.C_RED + err_msg +self.BOLD+ self.C_END)
+
+  def ExtractZipFile(self, zipped_file, dir_path):
+    """Extracts Zip File contents to current working directory.
+
+    Args:
+      zipped_file: Input zip file.
+      dir_path: Results directory path.
+    """
+    with zipfile.ZipFile(zipped_file, 'r') as zf:
+      zf.extractall(dir_path)
+
+  def CopyFileToDestination(self, src_path, src_file_name,
+                            dest_path, dest_file_name):
+    """Copies file from source path to destination path
+
+    Args:
+      src_path: source file path.
+      src_file_name: source file name.
+      dest_path: destination file path.
+      dest_file_name: destination file name.
+    """
+    src_file = os.path.join(src_path, src_file_name)
+    dest_file = os.path.join(dest_path, dest_file_name)
+    try:
+      shutil.copy(src_file, dest_file)
+    except IOError as e:
+      print('Unable to copy file. %s' % e)
+
+  def SplitFileName(self, file_path):
+    """Splits file to create file names with different extensions.
+
+    Args:
+      file_path: Input file path.
+      Sample Input File: ~/Downloads/Manual/2017.09.22_14.27.09.zip
+
+    Returns:
+      List of base file names with different extensions.
+      Sample base file names returned in split_file_list:
+      base: 2017.09.22_14.27.09.zip
+      basename: 2017.09.22_14.27.09
+      basename_xml_file: 2017.09.22_14.27.09.xml
+      basename_xml_gz_file: 2017.09.22_14.27.09.xml.gz
+    """
+    base = os.path.basename(file_path)
+    basename = os.path.splitext(base)[0]
+    basename_xml_file = '%s%s' % (basename, '.xml')
+    basename_xml_gz_file = '%s%s%s' % (basename, '.xml', '.gz')
+    return [base,
+            basename,
+            basename_xml_file,
+            basename_xml_gz_file]
+
+  def PrintFormattedDataCtsv(self, item):
+    """Prints build information in a tabular form to the terminal.
+
+    Sample table printed to terminal:
+    Build Board Name : Build Board Version : CTS VERIFIER VERSION
+    : relm           : R62-9901.20.0       : 7.1_r9
+
+    Args:
+       item: Information list to be printed to terminal.
+    """
+    print(('{:^%s}'%self.SPACE_COUNT_CTSV_BOARD_NAME).format(item[1]),
+          ('{:>%s}'%self.SPACE_COUNT_CTSV_BUILD_VERSION).format(item[2]),
+          ('{:>%s}'%self.SPACE_COUNT_VERIFIER_VERSION).format(item[3]))
+
+  def PrintFormattedDataCts(self, item):
+    """Prints build information in a tabular form to the terminal.
+
+    Sample table printed to terminal:
+    : Time Stamp         : Build Board Name : Build Version: Build BOARD ABI
+    : PACKAGE NAME           : FAILED  : PASSED
+    : 2017.09.22_14.27.09: relm             : R62-9901.20.0: x86
+    : CtsUsageStatsTestCases : 0       : 1
+
+    Args:
+      item: Information List to be printed to terminal.
+    """
+    print(('{:>%s}'%self.SPACE_COUNT_TIMESTAMP).format(item[0]),
+          ('{:^%s}'%self.SPACE_COUNT_CTS_BOARD_NAME).format(item[1]),
+          ('{:^%s}'%self.SPACE_COUNT_CTS_BUILD_VERSION).format(item[2]),
+          ('{:>%s}'%self.SPACE_COUNT_CTS_BOARD_ABI).format(item[3]),
+          ('{:^%s}'%self.SPACE_COUNT_PACKAGE_NAME).format(item[4]),
+          ('{:<%s}'%self.SPACE_COUNT_FAILED_TESTS).format(item[5]),
+          ('{:<%s}'%self.SPACE_COUNT_PASSED_TESTS).format(item[6]))
+
+  def GetXmlTagNamesCtsv(self):
+    """Get xml tag names.
+
+    Returns:
+      tag_list: List of xml tag names.
+    """
+    BUILD_N = 'Build'
+    RESULT = 'Result'
+    SUITE_VERSION = 'suite_version'
+    BUILD_BOARD = 'build_board'
+    BUILD_ID_N = 'build_id'
+    TEST = 'Test'
+    return [BUILD_N,
+            RESULT,
+            SUITE_VERSION,
+            BUILD_BOARD,
+            BUILD_ID_N,
+            TEST]
+
+  def GetXmlTagNamesCts(self, test_result_filename):
+    """Get xml tag names from M/N test result xml file.
+
+    Args:
+      test_result_filename: M/N Build Test Result file name.
+
+    Returns:
+      tag_list: List of xml tag names.
+    """
+    TEST_RESULT_FILENAME_M_BUILD = 'testResult.xml'
+    SUMMARY = 'Summary'
+    BUILD_M = 'BuildInfo'
+    BUILD_N = 'Build'
+    BUILD_BOARD_VERSION = 'build_board'
+    BUILD_ID_M = 'buildID'
+    BUILD_ID_N = 'build_id'
+    TEST_PACKAGE_LIST_M = 'TestPackage'
+    TEST_PACKAGE_LIST_N = 'Module'
+    TEST_PACKAGE_NAME_M = 'appPackageName'
+    TEST_PACKAGE_NAME_N = 'name'
+    BUILD_ABI_TYPE = 'abi'
+    FAILED = 'failed'
+    PASSED = 'pass'
+    if test_result_filename == TEST_RESULT_FILENAME_M_BUILD:
+      BUILD_VERSION = BUILD_M
+      BUILD_ID_TYPE = BUILD_ID_M
+      TEST_PACKAGE_LIST_VERSION = TEST_PACKAGE_LIST_M
+      TEST_PACKAGE_NAME_VERSION = TEST_PACKAGE_NAME_M
+    else:
+      BUILD_VERSION = BUILD_N
+      BUILD_ID_TYPE = BUILD_ID_N
+      TEST_PACKAGE_LIST_VERSION = TEST_PACKAGE_LIST_N
+      TEST_PACKAGE_NAME_VERSION = TEST_PACKAGE_NAME_N
+    return [SUMMARY,
+            BUILD_VERSION,
+            BUILD_BOARD_VERSION,
+            BUILD_ID_TYPE,
+            TEST_PACKAGE_LIST_VERSION,
+            TEST_PACKAGE_NAME_VERSION,
+            BUILD_ABI_TYPE,
+            FAILED,
+            PASSED]
+
+  def UpdateBuildInfoList(self, obj):
+    """Appends object to list.
+
+    Args:
+      obj: Contents to be appended to list.
+    """
+    self.build_info_list.append(obj)
+
+  def CreateApfeFolder(self, filename, dir_path):
+    """Creates hierarchy for uploading result files to APFE bucket.
+
+    Args:
+      filename: Valid file to upload to APFE folder.
+      dir_path: Results directory path.
+    """
+    build_board = ''
+    build_id = ''
+    split_file_list = self.SplitFileName(filename)
+    base = split_file_list[0]
+    build_board = self.build_info_list[-1][1]
+    build_id = self.build_info_list[-1][2]
+    release_folder = '%s-release' % build_board
+    build_folder = '%s' % build_id
+    manual_folder = 'manual'
+    apfe_path = os.path.join(dir_path, release_folder, build_folder,
+                             manual_folder)
+    if not os.path.exists(apfe_path):
+      os.makedirs(apfe_path)
+    self.CopyFileToDestination(dir_path, base, apfe_path, base)
+
+  def CreateCtsFolder(self, filename, dir_path):
+    """Creates hierarchy for upload to CTS Dashboard bucket.
+
+    Args:
+      filename: Input File to be added to CTS folder.
+      dir_path: Results directory path.
+    """
+    build_id = None
+    build_board = None
+    build_board = self.build_info_list[-1][1]
+    build_id = self.build_info_list[-1][2]
+    split_file_list = self.SplitFileName(filename)
+    basename_xml_file = split_file_list[2]
+    basename_xml_gz_file = split_file_list[3]
+    build_id_board_folder = '%s_%s' % (build_id, build_board)
+    cts_path = os.path.join(dir_path, build_id_board_folder)
+    if not os.path.exists(cts_path):
+      os.makedirs(cts_path)
+    gzip_file_path = os.path.join(dir_path, basename_xml_file)
+    check_call(['gzip', gzip_file_path])
+    self.CopyFileToDestination(dir_path,
+                               basename_xml_gz_file,
+                               cts_path,
+                               basename_xml_gz_file)
+
+  def CreateUntestedFolder(self, filename, dir_path):
+    """Creates Untested Folder to save files if there are UnTested Tests.
+
+    Args:
+      filename: Input file name.
+      dir_path: Results directory path.
+    """
+    split_file_list = self.SplitFileName(filename)
+    base = split_file_list[0]
+    if self.untested_tests_exist is True:
+      self.untested_file_count = self.untested_file_count + 1
+    untested_folder = 'Untested Tests'
+    untested_path = os.path.join(dir_path, untested_folder)
+    if not os.path.exists(untested_path):
+      os.makedirs(untested_path)
+    self.CopyFileToDestination(dir_path, base, untested_path, base)
+
+  def CreateObsoleteFolder(self, filename, dir_path):
+    """Creates Obsolete Folder to save files if there is a BUILDID mismatch.
+
+    Args:
+      filename: Input file name.
+      dir_path: Results directory path.
+    """
+    split_file_list = self.SplitFileName(filename)
+    base = split_file_list[0]
+    if self.build_mismatch_exist is True:
+      self.build_mismatch_file_count = self.build_mismatch_file_count + 1
+    if self.untested_tests_exist is True:
+      self.untested_file_count = self.untested_file_count + 1
+    obsolete_folder = 'BuildID Mismatch Folder'
+    obsolete_path = os.path.join(dir_path, obsolete_folder)
+    if not os.path.exists(obsolete_path):
+      os.makedirs(obsolete_path)
+    self.CopyFileToDestination(dir_path, base, obsolete_path, base)
diff --git a/provingground/cts/upload_cts.py b/provingground/cts/upload_cts.py
new file mode 100755
index 0000000..f21cbac
--- /dev/null
+++ b/provingground/cts/upload_cts.py
@@ -0,0 +1,277 @@
+#!/usr/bin/env python3
+#
+# Copyright 2017 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This script can be used to upload CTS results to CTS and APFE buckets."""
+
+from __future__ import print_function
+from lib.upload_utils import UploadUtils
+from xml.dom import minidom
+
+import argparse
+import getopt
+import os.path
+import sys
+
+
+class UploadCts(UploadUtils):
+  """Helps upload CTS results to gs:// buckets.
+
+  Prints CTS test results with their pass/fail status to terminal.
+  Prints gsutil links to the terminal to upload CTS results to gs:// buckets
+  to get the results in the tooling.
+
+  Attributes:
+    test_board_name: Test board name.
+    current_test_board_name: Temporary string to hold current test board name.
+    build_mismatch_exist: A boolean indicating if build mismatch exists.
+    failed_tests_exist: A boolean indicating if failed tests exist.
+    valid_exist: A boolean indicating if any valid file with the
+                 correct build version exists.
+    untested_tests_exist: A boolean indicating if untested tests exist.
+  """
+  def __init__(self, input_build_id, dir_path):
+    super(UploadCts, self).__init__()
+    self.test_board_name = None
+    self.current_test_board_name = None
+    self.build_mismatch_exist = False
+    self.valid_exist = False
+    self.untested_tests_exist = False
+    self.obsolete_files_exist = False
+    self.input_build_id = input_build_id
+    self.dir_path = dir_path
+
+  def PrintBuildInfoList(self):
+    """Sorts build information by build board and prints to the terminal."""
+    self.build_info_list.sort(key=lambda x: x[1])
+    for item in self.build_info_list:
+      if self.current_test_board_name != item[1]:
+        title_msg = 'List of files for %s' % item[1]
+        table_column_header = self.TABLE_HEADER_CTS
+
+        print(self.BOLD +
+              self.C_PURPLE +
+              title_msg +
+              self.BOLD +
+              self.C_END)
+        print(self.BOLD +
+              self.C_BLACK +
+              table_column_header +
+              self.BOLD +
+              self.C_END)
+
+        self.current_test_board_name = item[1]
+      self.PrintFormattedDataCts(item)
+    print('\n')
+
+  def PrintGsutilLinks(self, dir_path):
+    """Prints gsutil links to upload results to CTS and APFE buckets.
+
+    Args:
+      dir_path: Results directory path.
+    """
+    for item in self.build_info_list:
+      if self.test_board_name != item[1]:
+        print('gsutil cp -r {0}/{1}-release/ gs://chromeos-cts-apfe/'
+              .format(dir_path, item[1]))
+        print('gsutil cp -r {0}/{1}_{2}/ '
+              'gs://chromeos-cts-results/manual'
+              .format(dir_path, item[2], item[1]))
+        self.test_board_name = item[1]
+
+
+  def ParseXmlFile(self, abs_input_file_path, test_result_filename, dir_path):
+    """Gets build information for M or N Builds.
+
+    Parses the test result xml file to obtain build information. Adds build
+    information to a list. Handles Untested tests if they exist. Untested
+    tests may not have complete build information or may have partial
+    build information in the test_result.xml file.
+
+    Args:
+      abs_input_file_path: Absolute input file path.
+                           Eg: ~/Downloads/Manual/2017.09.22_14.27.09.zip
+      test_result_filename: M/N build test result file name.
+                            Eg: testResult.xml(M) or test_result.xml(N)
+      dir_path: Results directory path.
+                Eg: ~/Downloads/Manual
+    """
+    self.GetXmlTagNamesCts(test_result_filename)
+    tag_list = self.GetXmlTagNamesCts(test_result_filename)
+    summary = tag_list[0]
+    build_version = tag_list[1]
+    build_board_version = tag_list[2]
+    build_id_type = tag_list[3]
+    test_package_list = tag_list[4]
+    test_package_v = tag_list[5]
+    build_abi_type = tag_list[6]
+    failed = tag_list[7]
+    passed = tag_list[8]
+    split_file_list = self.SplitFileName(abs_input_file_path)
+    basename = split_file_list[1]
+    full_base_name = os.path.join(dir_path, basename)
+    basename_xml_file = split_file_list[2]
+    absolute_base_xml_file = os.path.join(dir_path, basename_xml_file)
+    timestamp = basename
+    self.CopyFileToDestination(full_base_name,
+                               test_result_filename,
+                               dir_path,
+                               basename_xml_file)
+    xml_doc = minidom.parse(absolute_base_xml_file)
+    summary = xml_doc.getElementsByTagName(summary)
+    build_info = xml_doc.getElementsByTagName(build_version)
+    test_package_list = xml_doc.getElementsByTagName(test_package_list)
+    if not build_info:
+      self.UpdateBuildInfoList([timestamp,
+                                'null',
+                                'null',
+                                'null',
+                                'null',
+                                '0',
+                                '0'])
+      self.untested_tests_exist = True
+    elif not build_info[0].hasAttribute(build_board_version):
+      self.UpdateBuildInfoList([timestamp,
+                                'null',
+                                'null',
+                                'null',
+                                'null',
+                                '0',
+                                '0'])
+      self.untested_tests_exist = True
+    elif not test_package_list:
+      self.UpdateBuildInfoList([timestamp,
+                                'null',
+                                'null',
+                                'null',
+                                'null',
+                                '0',
+                                '0'])
+      self.untested_tests_exist = True
+    else:
+      build_board = build_info[0].attributes[build_board_version].value
+      build_id = build_info[0].attributes[build_id_type].value
+      test_package = test_package_list[0].attributes[test_package_v].value
+      build_abi = test_package_list[0].attributes[build_abi_type].value
+      failed = summary[0].attributes[failed].value
+      passed = summary[0].attributes[passed].value
+      self.UpdateBuildInfoList([timestamp,
+                                build_board,
+                                build_id,
+                                build_abi,
+                                test_package,
+                                failed,
+                                passed])
+
+  def ProcessFilesToUpload(self, input_build_id, dir_path):
+    """Process Files to Upload to CTS and APFE Buckets.
+
+    Checks if Test Result File has M build or N Build and parses the
+    corresponding xml file to obtain build information.Checks for file
+    validity by comparing with provided input BuildID. Pushes valid files to
+    appropriate folders to upload to APFE and CTS buckets.Pushes invalid
+    files to Obsolete Folder.Pushes untested files if any to UnTested
+    Folder.
+
+    Args:
+      input_build_id: BuildID command line argument.
+      dir_path: Results directory path.
+    """
+    test_result_filename_n_build = 'test_result.xml'
+    test_result_filename_m_build = 'testResult.xml'
+    build_id = ' '
+    item = ' '
+    for the_file in os.listdir(dir_path):
+      if the_file.endswith('.zip'):
+        full_file_path = os.path.join(dir_path, the_file)
+        self.ExtractZipFile(full_file_path, dir_path)
+        split_file_list = self.SplitFileName(full_file_path)
+        basename = split_file_list[1]
+        full_base_name = os.path.join(dir_path, basename)
+        src_file = os.path.join(full_base_name,
+                                test_result_filename_m_build)
+        is_m_build = os.path.isfile(src_file)
+        if is_m_build is True:
+          self.ParseXmlFile(full_file_path,
+                            test_result_filename_m_build,
+                            dir_path)
+
+        else:
+          self.ParseXmlFile(full_file_path,
+                            test_result_filename_n_build,
+                            dir_path)
+
+        for item in self.build_info_list:
+          build_id = item[2]
+
+        if build_id == input_build_id:
+          self.valid_exist = True
+          self.CreateApfeFolder(full_file_path, dir_path)
+          self.CreateCtsFolder(full_file_path, dir_path)
+
+        else:
+          self.obsolete_files_exist = True
+          if self.untested_tests_exist is True:
+            self.build_info_list.remove(item)
+            self.CreateUntestedFolder(full_file_path, dir_path)
+            self.untested_tests_exist = False
+          else:
+            self.build_mismatch_exist = True
+            self.build_info_list.remove(item)
+            self.CreateObsoleteFolder(full_file_path, dir_path)
+
+  def PrintData(self, dir_path):
+    """Print results/gsutil links or error message if invalid files exist."""
+    if not self.obsolete_files_exist:
+      self.PrintBuildInfoList()
+      self.PrintGsutilLinks(dir_path)
+
+    if self.build_mismatch_exist is True and self.valid_exist is True:
+      self.PrintBuildInfoList()
+      self.PrintGsutilLinks(dir_path)
+      self.PrintObsoleteFolderCount()
+
+    if self.untested_file_count > 0 and self.valid_exist is True:
+      self.PrintBuildInfoList()
+      self.PrintGsutilLinks(dir_path)
+      self.PrintUntestedFolderCount()
+
+    if self.obsolete_files_exist is True and not self.valid_exist:
+      self.PrintObsoleteFolderCount()
+      self.PrintUntestedFolderCount()
+
+def main(argv):
+  """Processes result files and prints gsutil links to upload CTS results.
+
+  Displays help message when script is called without expected arguments.
+  Displays Usage: upload_cts.py <build_id> <results_dir_path>.
+  """
+  parser = argparse.ArgumentParser()
+  parser.add_argument('build_id',
+                      help='upload_cts.py <build_id> <results_dir_path>')
+  parser.add_argument('dir_path',
+                      help='upload_cts.py <build_id> <results_dir_path>')
+  args = parser.parse_args()
+
+  cts = UploadCts(args.build_id,args.dir_path)
+
+  if len(sys.argv) == 1:
+    parser.print_help()
+    sys.exit(1)
+  parser.parse_args()
+  try:
+    opts, args = getopt.getopt(argv, 'hb:', ['build_id='])
+  except getopt.GetoptError:
+    sys.exit(2)
+  for opt in opts:
+    if opt == '-h':
+      cts.usage()
+      sys.exit()
+  cts.ProcessFilesToUpload(cts.input_build_id, cts.dir_path)
+  os.system('stty cols 120')
+  cts.PrintData(cts.dir_path)
+
+if __name__ == '__main__':
+  main(sys.argv[1:])
diff --git a/provingground/cts/upload_cts_verifier.py b/provingground/cts/upload_cts_verifier.py
new file mode 100755
index 0000000..12e8e84
--- /dev/null
+++ b/provingground/cts/upload_cts_verifier.py
@@ -0,0 +1,214 @@
+#!/usr/bin/env python3
+#
+# Copyright 2017 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This script can be used to upload CTS Verifier results to CTS Dashboard."""
+
+from __future__ import print_function
+from lib.upload_utils import UploadUtils
+from xml.dom import minidom
+
+import argparse
+import getopt
+import os.path
+import sys
+
+
+class UploadCtsVerifier(UploadUtils):
+  """Helps upload CTS-V results to gs:// buckets.
+
+  Prints CTS-V test results with their pass/fail status to terminal.
+  Prints gsutil links to the terminal to upload CTS-V results to gs:// buckets
+  to get the results in the tooling.
+
+  Attributes:
+    build_mismatch_exist: A boolean indicating if build mismatch exists.
+    failed_tests_exist: A boolean indicating if failed tests exist.
+    valid_files_exist: A boolean indicating if any valid file with the
+                       correct build version exists.
+    untested_tests_exist: A boolean indicating if untested tests exist.
+    test_board_name_list: List of test board names.
+  """
+
+  def __init__(self):
+    super(UploadCtsVerifier, self).__init__()
+    self.build_mismatch_exist = False
+    self.failed_tests_exist = False
+    self.valid_files_exist = False
+    self.untested_tests_exist = False
+    self.test_board_name_list = []
+
+  def PrintBuildInfoList(self):
+    """Sorts build information by build board and prints to the terminal."""
+    self.build_info_list.sort(key=lambda x: x[1])
+    for item in self.build_info_list:
+      if item[1] not in self.test_board_name_list:
+        title_msg = 'List of files for %s' % item[1]
+        table_column_header = self.TABLE_HEADER_CTSV
+        print(self.BOLD +
+              self.C_PURPLE +
+              title_msg +
+              self.BOLD +
+              self.C_END)
+        print(self.BOLD +
+              self.C_BLACK +
+              table_column_header +
+              self.BOLD +
+              self.C_END)
+
+        self.test_board_name_list.append(item[1])
+        self.PrintFormattedDataCtsv(item)
+    print('\n')
+
+  def PrintGsutilLinks(self, dir_path):
+    """Prints gsutil links to upload results to CTS-V dashboard bucket.
+
+    Args:
+      dir_path: Results directory path.
+    """
+    for item in self.build_info_list:
+      gsutil_msg = ('gsutil cp -r {0}/{1}_{2}/ '
+                    'gs://chromeos-cts-results/ctsVerifier'
+                    .format(dir_path, item[2], item[1]))
+      print(self.BOLD + self.C_PURPLE + gsutil_msg + self.BOLD + self.C_END)
+
+  def PrintIfAllTestsPassed(self):
+    """Print message to terminal if all tests have passed."""
+    if not self.failed_tests_exist and self.build_mismatch_file_count == 0:
+      print('All Cts Verifier Tests passed!')
+
+  def PrintFailedTestResults(self, tests):
+    """Prints failed results if any to the terminal.
+
+    Args:
+      tests: CTS Verifier Test Results.
+    """
+    test_title = ' '
+    result = ' '
+    for test in tests:
+      test_title = test.getAttribute('name')
+      result = test.getAttribute('result')
+      if result == 'fail' or result == 'not-executed':
+        self.failed_tests_exist = True
+        print('Test Title:%s, Result:%s' % (test_title, result))
+        print('\n')
+
+  def ParseXmlFile(self, abs_input_file_path, dir_path):
+    """Gets build information from CTS Verifier xml file.
+
+    Parses the CTS Verifier Xml File to obtain Build Information such as
+    basename, build_board, build_id, suite_version.
+
+    Args:
+      abs_input_file_path: Absolute Input file path.
+                           Eg: ~/Downloads/ctsv/2017.09.21_17.13.04-
+                           CTS_VERIFIER-google-relm-relm_cheets-R62-9901.20.0
+      dir_path: Results directory path.
+                Eg: ~/Downloads/Manual
+
+    Returns:
+      tests: CTS Verifier Test Results.
+    """
+    test_result_filename_n_build = 'test_result.xml'
+    tag_list = self.GetXmlTagNamesCtsv()
+    build_version = tag_list[0]
+    result = tag_list[1]
+    suite_version = tag_list[2]
+    build_board = tag_list[3]
+    build_id_n = tag_list[4]
+    test = tag_list[5]
+    split_file_list = self.SplitFileName(abs_input_file_path)
+    basename = split_file_list[1]
+    full_base_name = os.path.join(dir_path, basename)
+    basename_xml_file = split_file_list[2]
+    absolute_base_xml_file = os.path.join(dir_path, basename_xml_file)
+    self.CopyFileToDestination(full_base_name,
+                               test_result_filename_n_build,
+                               dir_path,
+                               basename_xml_file)
+    xml_doc = minidom.parse(absolute_base_xml_file)
+    build = xml_doc.getElementsByTagName(build_version)
+    verifier_info = xml_doc.getElementsByTagName(result)
+    suite_version = verifier_info[0].attributes[suite_version].value
+    build_board = build[0].attributes[build_board].value
+    build_id = build[0].attributes[build_id_n].value
+    tests = xml_doc.getElementsByTagName(test)
+    self.UpdateBuildInfoList(
+        [basename, build_board, build_id, suite_version])
+    return tests
+
+  def ProcessFilesToUpload(self, input_build_id, dir_path):
+    """Process Files to Upload to CTS Verifier Bucket.
+
+    Parses the test_result.xml file to obtain build information.
+    Pushes valid files to appropriate folders to upload to CTS-V bucket.
+    Pushes invalid files to Obsolete Folder.
+    Print Build results to terminal.
+
+    Args:
+      input_build_id: Build ID for which results are to be uploaded.
+      dir_path: Results directory path.
+    """
+    build_id = ' '
+    item = ' '
+    for the_file in os.listdir(dir_path):
+      if the_file.endswith('.zip'):
+        full_file = os.path.join(dir_path, the_file)
+        self.ExtractZipFile(full_file, dir_path)
+        tests = self.ParseXmlFile(the_file, dir_path)
+        for item in self.build_info_list:
+          build_id = item[2]
+        if build_id == input_build_id:
+          self.valid_files_exist = True
+          self.failed_tests_exist = False
+          self.CreateCtsFolder(full_file, dir_path)
+          self.PrintBuildInfoList()
+          self.PrintFailedTestResults(tests)
+          self.PrintIfAllTestsPassed()
+        else:
+          self.build_mismatch_exist = True
+          self.build_info_list.remove(item)
+          self.CreateObsoleteFolder(full_file, dir_path)
+
+def main(argv):
+  """Processes result files and prints gsutil links to upload CTS-V results.
+
+    Displays help message when script is called without expected arguments.
+    Displays Usage: upload_cts_verifier.py <build_id> <results_dir_path>
+  """
+  parser = argparse.ArgumentParser()
+  parser.add_argument('build_id',
+                      help='Usage: upload_cts_verifier.py'
+                           '<build_id>'
+                           '<results_dir_path>')
+  parser.add_argument('dir_path',
+                      help='Usage: upload_cts_verifier.py'
+                           '<build_id>'
+                           '<results_dir_path>')
+  args = parser.parse_args()
+  input_build_id = args.build_id
+  dir_path = args.dir_path
+  if len(sys.argv) == 1:
+    parser.print_help()
+    sys.exit(1)
+  parser.parse_args()
+  try:
+    opts, args = getopt.getopt(argv, 'hb:', ['buildID='])
+  except getopt.GetoptError:
+    print('upload_cts_verifier.py <build_id> <results_dir_path>')
+    sys.exit(2)
+  ctsv = UploadCtsVerifier()
+  for opt in opts:
+    if opt == '-h':
+      ctsv.usage()
+      sys.exit()
+  ctsv.ProcessFilesToUpload(input_build_id, dir_path)
+  if ctsv.valid_files_exist is True:
+    ctsv.PrintGsutilLinks(dir_path)
+  if ctsv.build_mismatch_exist is True:
+    ctsv.PrintObsoleteFolderCount()
+
+if __name__ == '__main__':
+  main(sys.argv[1:])