Update browsertest_status.py to work with ToT and branch builders.

This update allows the browsertest_status.py to work with both ToT and
Branch builders. To accommodate this, it adds two command line flags
to specify:
- Build Host
- Build Project
- Chromium Version

In an unrelated change, it adds a flag to enable creating a report
file, which is now disabled by default.

BUG=chromium:375395
TEST=Put a tests file in the same directory as the script, that lists
the browsertests you want to have a status/results report. Then run:
$ ./browsertest_status.py --tests_file=tests

Change-Id: I7fd5a1516aaacc2422ea6ff784400843ea6bbe23
Reviewed-on: https://chromium-review.googlesource.com/262432
Reviewed-by: Scott Cunningham <scunningham@chromium.org>
Commit-Queue: Scott Cunningham <scunningham@chromium.org>
Tested-by: Scott Cunningham <scunningham@chromium.org>
diff --git a/provingground/browsertest_status.py b/provingground/browsertest_status.py
index 5dae11c..7bac916 100755
--- a/provingground/browsertest_status.py
+++ b/provingground/browsertest_status.py
@@ -1,26 +1,26 @@
-#!/usr/bin/python
-# Copyright (c) 2015 The Chromium OS Authors. All rights reserved.
+#!/usr/bin/python2
+# Copyright 2015 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.
 
 # Script to display latest test run status and test results for a user-
-# specified list of browser_tests. The names of the desired brower_tests are
+# specified list of browsertests. The names of the desired browertests are
 # read from a file located (by default) in the same directory as this script.
 #
-# Latest test run status is fetched from the build.chromium.org build server,
-# read from the 'stdio' text file located at:
-#   http://build.chromium.org/p/chromium.chromiumos/builders/TR_BUILDER/
-#   builds/BUILD_NUMBER/steps/browser_tests/logs/stdio/text
+# Latest test run status for a build is fetched from the builder, read from
+# the 'stdio' text file located at:
+#   http://BUILDER_HOST/p/BUILDER_PROJECT/builders/BUILDER_NAME/
+#   builds/BUILD_NUMBER/steps/TEST_TYPE/logs/stdio/text
 #
-# Recent test results are fetched from the test-results.appspot.com server,
-# read from the results.json file located at:
-#   https://test-results.appspot.com/testfile?master=ChromiumChromiumOS&
-#   builder=Linux ChromiumOS Tests (2)&testtype=browser_tests&
-#   name=results.json
+# Recent test results history are fetched from the test-results server, read
+# from the results.json file located at:
+#   https://TR_HOST/testfile?master=TR_MASTER&builder=BUILDER_NAME
+#   &testtype=TEST_TYPE&name=results.json
 #
 
 """Script to report test status and results of user-specified browsertests."""
-__author__ = ('scunningham@google.com Scott Cunningham')
+
+from __future__ import print_function
 
 import argparse
 import json
@@ -29,16 +29,18 @@
 import sys
 import urllib2
 
-# Chromium builder url parameter defaults.
-_BUILD_HOST = 'build.chromium.org'
-_BUILD_PROJECT = 'chromium.chromiumos'
-_BUILD_NUMBER = '-2'
+__author__ = 'scunningham@google.com (Scott Cunningham)'
+
+# Builder url parameter defaults.
+_BUILDER_HOST = 'chromegw.corp.google.com'  # Host name of Builder.
+_BUILDER_PROJECT = 'chromeos.chrome'  # Project name of Builder.
+_BUILDER_NAME = 'Linux ChromeOS Buildspec Tests'  # Builder name.
+_BUILD_NUMBER = -1
 _TEST_TYPE = 'browser_tests'
 
-# TestResults server url parameter defaults.
-_TR_HOST = 'test-results.appspot.com'  # URI to TestResults server.
-_TR_MASTER = 'ChromiumChromiumOS'  # Test-results build master repository.
-_TR_BUILDER = 'Linux ChromiumOS Tests (dbg)(1)'  # TestResults builder name.
+# Test-results server url parameter defaults.
+_TR_HOST = 'test-results.appspot.com'  # Host name of test-results server.
+_TR_MASTER = 'ChromiumChromiumOS'  # Project master of test-results server.
 
 # Input file and report directory parameter defaults.
 _TESTS_FILE = './tests'  # Path to the file that contains the tests names.
@@ -68,91 +70,8 @@
 _MISSING = 'Missing'
 
 
-def _FindLatestCompletedBuildNumber(build_number):
-  """Find the latest completed build number from the given build number.
-
-  Check if build with build_number completed successfully. If not, iteratively
-  check earlier versions, until a successfully completed build is found.
-
-  Args:
-    build_number: build number to start search.
-
-  Returns:
-    Build number of most recent successfully completed build.
-  """
-  # TODO(scunningham): Implement a real find function in later version.
-  return build_number
-
-
-def _GetStdioLogUrl(builder, build_num, test_type):
-  """Get url to Stdio Log file from builder for build number.
-
-  Args:
-    builder: Builder name.
-    build_num: Build number.
-    test_type: Type of browser test.
-
-  Returns:
-    Url to the Stdio log text file.
-  """
-  # Generate percent-encoded build url.
-  build_url = (('http://%s/p/%s/json/builders/%s/builds?'
-                'select=%s/steps/%s/') %
-               (urllib2.quote(_BUILD_HOST), urllib2.quote(_BUILD_PROJECT),
-                urllib2.quote(builder), urllib2.quote(build_num),
-                urllib2.quote(test_type)))
-
-  # Fetch build status file from build url.
-  print '\nFetching build status file from: %s' % build_url
-  url = urllib2.urlopen(build_url)
-  build_status_file = url.read()
-  url.close()
-
-  # Convert json build status file to dictionary.
-  build_status_dict = json.loads(build_status_file)
-
-  # Extract stdio log url from build status dictionary.
-  print '\n  Contents of build status file: %s' % build_status_dict
-  return build_status_dict[build_num]['steps'][test_type]['logs'][0][1]
-
-
-def _GetStdioLogTestsDict(stdio_url):
-  """Get Stdio Log browser_tests from the given url.
-
-  Args:
-    stdio_url: url to stdio log browser_tests file.
-
-  Returns:
-    Dictionary of tests from stdio log browser_tests text file.
-  """
-  # Fetch builder stdio log text file from url.
-  stdio_text_url = stdio_url+'/text'
-  print '\nFetching builder stdio log file from: %s' % stdio_text_url
-  stdio_text_file = urllib2.urlopen(stdio_text_url)
-
-  # Extract test lines from stdio text file.
-  pattern = r'\[\d+/\d+\] '
-  test_lines = [line for line in stdio_text_file if re.match(pattern, line)]
-  stdio_text_file.close()
-  print '  Total run tests extracted: %s' % len(test_lines)
-  if test_lines:
-    print '  Last run test line: "%s"' % test_lines[-1].strip()
-
-  # Extract test data and pack into stdio tests dictionary.
-  stdio_tests_dict = {}
-  pattern = r'(\[\d*/\d*\]) (.*?) (\(.*\))'
-  for i, line in enumerate(test_lines):
-    m = re.match(pattern, line)
-    if m:
-      stdio_tests_dict[m.group(2)] = (m.group(1), m.group(3))
-    else:
-      print 'Error: Invalid test line %s) %s' % (i, line.strip())
-
-  return stdio_tests_dict
-
-
 def _GetUserSpecifiedTests(tests_file):
-  """Get list of user-specified tests from tests file.
+  """Get list of user-specified tests from the given tests_file.
 
   File must be a text file, formatted with one line per test. Leading and
   trailing spaces and blanklines are stripped from the test list. If a line
@@ -164,19 +83,426 @@
   Returns:
     List of user tests read from lines in the tests_file.
   """
-  print '\nFetching user-specified tests from: %s' % tests_file
+  print('\nReading user-specified tests from: %s' % tests_file)
   content = open(tests_file, 'r').read().strip()
-  return [line.strip().split()[0] for line in content.splitlines()
-          if line.strip()]
+  user_tests = [line.strip().split()[0] for line in content.splitlines()
+                if line.strip()]
+  if not user_tests:
+    print('Error: tests file is empty.')
+    sys.exit(2)
+  print('  Number of user tests: %s' % len(user_tests))
+
+  return user_tests
+
+
+def _GetBuildsList(build_dict):
+  """Get list of available (cached) builds on the builder.
+
+  Args:
+    build_dict: build info: builder host, project, and name.
+
+  Returns:
+    List of builds available on builder.
+  """
+  # Generate percent-encoded builder url.
+  builder_url = ('https://%s/p/%s/json/builders/%s' %
+                 (urllib2.quote(build_dict['builder_host']),
+                  urllib2.quote(build_dict['builder_proj']),
+                  urllib2.quote(build_dict['builder_name'])))
+
+  # Fetch builder status file from builder url.
+  print('\nFetching builder status file from: %s' % builder_url)
+  try:
+    response = urllib2.urlopen(builder_url)
+    builder_status_file = response.read()
+    response.close()
+  except urllib2.HTTPError as err:
+    print('HTTP error: %s' % err)
+    sys.exit(2)
+  except urllib2.URLError as err:
+    print('Notice: Builder was not available: %s' % err)
+    sys.exit(2)
+
+  # Convert json builder status file to dictionary.
+  builder_status_dict = json.loads(builder_status_file)
+  build_list = builder_status_dict['cachedBuilds']
+  print('\nList of avaiable builds: %s\n' % build_list)
+  return build_list
+
+
+def _FindBuildByChromiumVersion(build_dict):
+  """Find the latest completed build with the given chromium version.
+
+  Search from the top of the cached build list for a build with the given
+  chromium |version| number as it's buildspec_version. If found, return the
+  build number. If no build is found containing the |version|, return None.
+
+  Args:
+    build_dict: build info, with chromium version number.
+
+  Returns:
+    Number of most recent build containing the given chromium version.
+  """
+  # TODO(scunningham): Implement real function in later version.
+  cr_version = build_dict['cr_version']
+  if cr_version:
+    pass
+  return _BUILD_NUMBER
+
+
+def _GetNominalBuildNumber(build_num, builds_list):
+  """Verify the build number is in the available builds list.
+
+  If the user-specified |build_num| is negative, then verify it is a valid
+  ordinal index, and get the nominal build number for that index. If it is
+  positive, then verify the nominal build number exists in the |builds_list|.
+  Return the nominal build number. Note that if a build number is available
+  does not mean that the build is valid or completed.
+
+  Args:
+    build_num: nominal build number (positive) or ordinal index (negative).
+    builds_list: List of available build numbers on the builder.
+
+  Returns:
+    Nominal build number in |builds_list|.
+  """
+  # Verify that build number exists in the builds list.
+  if build_num < 0:  # User entered an index, not a build number.
+    if len(builds_list) < -build_num:
+      print('Error: The build_num index %s is too large.' % build_num)
+      sys.exit(2)
+    build_number = builds_list[build_num]
+    print('\n  Index %s selects build %s' % (build_num, build_number))
+    build_num = build_number
+  elif build_num not in builds_list:
+    print('Error: build %s is not available.' % build_num)
+    sys.exit(2)
+  return build_num
+
+
+def _LatestCompletedBuild(build_dict, builds_list):
+  """Find the latest completed build number from the given list of builds.
+
+  Check each build in the |builds_list|, starting with the build number in
+  the given |build_dict|, to determine if the build completed successfully.
+  If completed successfully, return the number of the build. If not, continue
+  checking. If none of the builds completed successfully, exits.
+
+  Args:
+    build_dict: build info dictionary, with build number to start search.
+    builds_list: List of cached build numbers on the builder.
+
+  Returns:
+    Build number of the latest successfully completed build, and the build
+    test status dictionary of that build.
+  """
+  # Find the latest completed build, starting from build_num.
+  build_num = build_dict['build_num']
+  starting_build_num = build_num
+  starting_build_num_failed = False
+  starting_build_index = builds_list.index(starting_build_num)
+  for build_num in reversed(builds_list[0:starting_build_index+1]):
+    build_test_status_dict = _BuildIsCompleted(build_dict, build_num)
+    if build_test_status_dict is not None:
+      break
+    starting_build_num_failed = True
+  else:
+    print('No completed builds are available.')
+    sys.exit(2)
+  if starting_build_num_failed:
+    print('\nError: Requested build_num %s was not completed successfully.' %
+          starting_build_num)
+  print('Using latest successfully completed build: %s\n' % build_num)
+  return build_num, build_test_status_dict
+
+
+def _BuildIsCompleted(build_dict, build_num):
+  """Determine whether build was completed successfully.
+
+  Get the build test status. Check whether the build given by |build_num]
+  was terminated by an error, or is not finished building. If so, return
+  None. Otherwise, return the build test status dictionary.
+
+  Args:
+    build_dict: build info dictionary.
+    build_num: build number to check.
+
+  Returns:
+    Dictionary of build test status if build is completed. Otherwise, None.
+  """
+
+  # Copy original build_dict, and point copy to |build_num|.
+  temp_build_dict = dict(build_dict)
+  temp_build_dict['build_num'] = build_num
+
+  # Get build test status dictionary from builder for |build_num|.
+  build_test_status_dict = _GetBuildTestStatus(temp_build_dict)
+  steps_dict = build_test_status_dict[str(build_num)]['steps']
+  test_type_dict = steps_dict[build_dict['test_type']]
+
+  # Check if build failed or threw an exception:
+  if 'error' in test_type_dict:
+    return None
+
+  # Check isFinished status in test_type_dict.
+  if test_type_dict['isFinished']:
+    return build_test_status_dict
+
+  return None
+
+
+def _GetBuildTestStatus(build_dict):
+  """Get the build test status for the given build.
+
+  Fetch the build test status file from the builder for the build number,
+  specified in the build info dictionary given by |build_dict|.
+
+  Args:
+    build_dict: Build info dictionary.
+
+  Returns:
+    Build Test Status dictionary.
+  """
+  # Generate percent-encoded build test status url.
+  build_url = (('https://%s/p/%s/json/builders/%s/builds?'
+                'select=%s/steps/%s/') %
+               (urllib2.quote(build_dict['builder_host']),
+                urllib2.quote(build_dict['builder_proj']),
+                urllib2.quote(build_dict['builder_name']),
+                build_dict['build_num'],
+                urllib2.quote(build_dict['test_type'])))
+  print('Fetching build test status file from: %s' % build_url)
+  return _FetchStatusFromUrl(build_dict, build_url)
+
+
+def _GetBuildStatus(build_dict):
+  """Get the build status for the given build.
+
+  Fetch the build status file from the builder for the build number, specified
+  in the build info dictionary given by |build_dict|.
+
+  Args:
+    build_dict: Build info dictionary.
+
+  Returns:
+    Build Status dictionary.
+  """
+  # Generate percent-encoded build status url.
+  build_url = (('https://%s/p/%s/json/builders/%s/builds/%s') %
+               (urllib2.quote(build_dict['builder_host']),
+                urllib2.quote(build_dict['builder_proj']),
+                urllib2.quote(build_dict['builder_name']),
+                build_dict['build_num']))
+  print('Fetching build status file from: %s' % build_url)
+  return _FetchStatusFromUrl(build_dict, build_url)
+
+
+def _FetchStatusFromUrl(build_dict, build_url):
+  """Get the status from the given URL.
+
+  Args:
+    build_dict: Build info dictionary.
+    build_url: URL to the status json file.
+
+  Returns:
+    Status dictionary.
+  """
+  # Fetch json status file from build url.
+  hosted_url = _SetUrlHost(build_url, build_dict['builder_host'])
+  url = urllib2.urlopen(hosted_url)
+  status_file = url.read()
+  url.close()
+
+  # Convert json status file to dictionary.
+  status_dict = json.loads(status_file)
+  return status_dict
+
+
+def _PrintChromiumVersion(build_properties):
+  """Get and print the version number of chromium used in the build.
+
+  Args:
+    build_properties: The properties dictionary for the build.
+
+  Returns:
+    A string containing the chromium version.
+  """
+  for property_list in build_properties:
+    if 'buildspec_version' in property_list:
+      chromium_version = property_list[1]
+      print('  Chromium version: %s' % chromium_version)
+      break
+  else:
+    chromium_version = None
+    print('  Warning: Build properties has no chromium version.')
+  return chromium_version
+
+
+def _GetTestsFailedList(build_dict, build_status_dict):
+  """Get list of failed tests for given build.
+
+  Extract test status summary, including number of tests disabled, flaky, and
+  failed, from the test step in |build_status_dict|. If the test step was
+  finished, then create a list of tests that failed from the failures string.
+
+  Args:
+    build_dict: Build info dictionary.
+    build_status_dict: Build status dictionary.
+
+  Returns:
+    List of tests that failed.
+  """
+  # Get test type from build_dict and tests steps from build_status_dict.
+  test_type = build_dict['test_type']
+  build_steps = build_status_dict['steps']
+
+  # Get count of disabled, flaky, failed, and test failures.
+  num_tests_disabled = 0
+  num_tests_flaky = 0
+  num_tests_failed = 0
+  test_failures = ''
+
+  for step_dict in build_steps:
+    text_list = step_dict['text']
+    if test_type in text_list:
+      for item in text_list:
+        m = re.match(r'(\d+) disabled', item)
+        if m:
+          num_tests_disabled = m.group(1)
+          continue
+        m = re.match(r'(\d+) flaky', item)
+        if m:
+          num_tests_flaky = m.group(1)
+          continue
+        m = re.match(r'failed (\d+)', item)
+        if m:
+          num_tests_failed = m.group(1)
+          continue
+        m = re.match(r'<br\/>failures:<br\/>(.*)<br\/>', item)
+        if m:
+          test_failures = m.group(1)
+          continue
+      break #  Exit step_dict loop if test_type is in text_list.
+    is_finished = step_dict['isFinished']
+  else:
+    print('Error: build_steps has no \'%s\' step.' % test_type)
+    is_finished = 'Error'
+
+  # Split the test_failures into a tests_failed_list.
+  tests_failed_list = []
+  if num_tests_failed:
+    tests_failed_list = test_failures.split('<br/>')
+
+  print('  Test finished: %s' % is_finished)
+  print('  Disabled: %s' % num_tests_disabled)
+  print('  Flaky: %s' % num_tests_flaky)
+  print('  Failed: %s : %s' % (num_tests_failed, tests_failed_list))
+
+  return tests_failed_list
+
+
+def _GetStdioLogUrlFromBuildStatus(build_dict, build_test_status_dict):
+  """Get url to Stdio Log file from given build test status dictionary.
+
+  Args:
+    build_dict: Build info dictionary.
+    build_test_status_dict: Build Test Status dictionary.
+
+  Returns:
+    Url to the Stdio Log text file.
+  """
+
+  steps_dict = build_test_status_dict[str(build_dict['build_num'])]['steps']
+  test_type_dict = steps_dict[build_dict['test_type']]
+
+  if 'error' in test_type_dict:
+    return None
+
+  log_url = test_type_dict['logs'][0][1]
+  stdio_log_url = _SetUrlHost(log_url, build_dict['builder_host'])+'/text'
+  return stdio_log_url
+
+
+def _GetStdioLogTests(stdio_log_url, tests_failed_list):
+  """Get Stdio Log Tests from the given url.
+
+    Extracts tests from the stdio log file of the builder referenced by
+    |stdio_log_url|, and packs them into a stdio tests dictionary. This
+    dictionary uses the long test name as the key, and the value is a list
+    that contains the test result. We use a list for the test result to mirror
+    the format used by the test-result server.
+
+    If a test is in the |tests_failed_list|, then set the test result to the
+    the failure code: 'Q'. Otherwise, set result to the pass code: 'P'. The
+    result repetition count of '999' is a placeholder that indicates that the
+    value is not from the test-result server.
+
+    Here is the format of the dictionary:
+    {
+      MaybeSetMetadata/SafeBrowseService.MalwareImg/1: [[999, 'P']],
+      MaybeSetMetadata/SafeBrowseService.MalwareImg/2: [[999, 'Q']],
+      PlatformAppBrowserTest.ComponentBackgroundPage: [[999, 'P']],
+      NoSessionRestoreTest.LocalStorageClearedOnExit: [[999, 'P']]
+    }
+
+  Args:
+    stdio_log_url: url to stdio log tests text file.
+    tests_failed_list: list of failed tests, from the build status page.
+
+  Returns:
+    Dictionary of test instances from stdio log tests text file.
+  """
+  # Fetch builder stdio log text file from url.
+  print('\nFetching builder stdio log file from: %s' % stdio_log_url)
+  stdio_text_file = urllib2.urlopen(stdio_log_url)
+
+  # Extract test lines from stdio log text file to test_lines dictionary.
+  p_test = r'\[\d*/\d*\] (.*?) \(.*\)'
+  p_exit = r'exit code \(as seen by runtest.py\)\: (\d+)'
+  test_lines = []
+  exit_flag = False
+  exit_code = None
+
+  for line in stdio_text_file:
+    if not exit_flag:
+      m = re.match(p_test, line)
+      if m:
+        if line not in test_lines:
+          test_lines.append(line)
+      m = re.match(p_exit, line)
+      if m:
+        exit_flag = True
+        exit_code = m.group(1)
+  stdio_text_file.close()
+  print('  Total run tests extracted: %s' % len(test_lines))
+  if test_lines:
+    print('  Last run test line: "%s"' % test_lines[-1].strip())
+
+  # Extract test_lines data and pack into stdio tests dictionary.
+  stdio_tests_dict = {}
+  for i, line in enumerate(test_lines):
+    m = re.match(p_test, line)
+    if m:
+      long_test_name = m.group(1)
+      if long_test_name in tests_failed_list:
+        test_result = [[999, u'Q']]  # Test result Failed code 'Q'.
+      else:
+        test_result = [[999, u'P']]  # Test result Passed code 'P'.
+      stdio_tests_dict[long_test_name] = test_result
+    else:
+      print('Error: Invalid test line %s) %s' % (i, line.strip()))
+
+  print('  Test Exit Code: %s' % exit_code)
+  return stdio_tests_dict
 
 
 def _RunAndNotrunTests(stdio_tests, user_tests):
-  """Return lists of individual test instances of user-specified tests.
+  """Return lists of run and not-run instances of given user tests.
 
-  The first list is of test instances present in the stdio tests list.
-  Presence indicates that the test instance was run on the build. Second list
-  is tests that are absent from the stdio tests list. Absence means that no
-  instance of the test was run on the build.
+  The first list is of test instances present in the |stdio_tests| list.
+  Presence indicates that the test instance was run on the build. The second
+  list is tests that are absent from the |stdio_tests| list. Absence means
+  that no instance of the test was run on the build.
 
   Note that there can be multiple instances of a user-specified test run on a
   build if a) The test belongs to test group, and b) the test was run with
@@ -188,13 +514,13 @@
     user_tests: List of test names specified by user.
 
   Returns:
-  1) run_tests: list of test instances of user tests run on build.
-  2) notrun_tests: list of user tests not run on build.
+    1) run_tests: list of test instances of user tests run on build.
+    2) notrun_tests: list of user tests not run on build.
   """
   run_user_tests = []
   notrun_user_tests = []
   for user_test in user_tests:
-    pattern = r'(.*/)?%s(/\d*)?$' % user_test
+    pattern = r'(.*/)?%s(/\d*)?$' % user_test  # pattern for test name.
     found_run_test = False
     for stdio_test in stdio_tests:
       if re.search(pattern, stdio_test):
@@ -202,51 +528,55 @@
         run_user_tests.append(stdio_test)
     if not found_run_test:
       notrun_user_tests.append(user_test)
-  print '  Run instances of user tests: %s' % len(run_user_tests)
-  print '  Not run user tests: %s\n' % len(notrun_user_tests)
+  print('  User tests: Instances run: %s' % len(run_user_tests))
+  print('  User tests: Not run: %s\n' % len(notrun_user_tests))
   return run_user_tests, notrun_user_tests
 
 
-def _GetResultsDict(master, builder, test_type):
-  """Get results dictionary from builder results.json file.
+def _GetTestResultsJson(master, builder_name, test_type):
+  """Get test results data from results.json file for the builder.
 
-  The results dictionary contains information about recent tests run,
-  test results, build numbers, chrome revision numbers, etc, for the
-  last 500 builds on the specified builder.
+  The results.json file contains historical data about the tests run on the
+  given |builder_name| for the most recent (up to the last 500) builds. The
+  data includes test names, test result codes, build numbers, and chrome
+  revision numbers.
 
   Args:
     master: Master repo (e.g., 'ChromiumChromiumOS')
-    builder: Builder name (e.g., 'Linux ChromiumOS Tests (2)')
-    test_type: Type of browser test.
+    builder_name: Builder name (e.g., 'Linux ChromiumOS Tests (dbg)(1)')
+    test_type: Type of browsertests: browser_tests or interactive_ui_tests
 
   Returns:
-    Dictionary of builder results.
+    Contents of the results.json file from the test-result server for the
+    specified builder.
   """
-  # Generate percent-encoded builder results url.
+  # Generate percent-encoded test results url for specified builder.
   results_url = (('https://%s/testfile?master=%s&builder=%s'
                   '&testtype=%s&name=results.json') %
                  (urllib2.quote(_TR_HOST), urllib2.quote(master),
-                  urllib2.quote(builder), urllib2.quote(test_type)))
+                  urllib2.quote(builder_name), urllib2.quote(test_type)))
 
-  # Fetch results file from builder results url.
-  print 'Fetching builder results file from %s' % results_url
-  url = urllib2.urlopen(results_url)
-  results_json = url.read()
-  url.close()
-
-  # Convert json results to native Python object.
-  builder_results_dict = json.loads(results_json)
-  return builder_results_dict[builder]
+  # Fetch results file from test results url.
+  print('Fetching test results file from %s' % results_url)
+  try:
+    url = urllib2.urlopen(results_url)
+    results_json = url.read()
+    url.close()
+  except urllib2.HTTPError:
+    results_json = None
+    print(('  Warning: test-result history was not available '
+           'for builder \'%s\'.\n' % builder_name))
+  return results_json
 
 
-def _CreateTestsResultsDictionary(builder_tests_dict):
-  """Create dictionary of all tests+results from builder tests dictionary.
+def _CreateTestsResultsDictionary(tr_tests_dict):
+  """Create dictionary of all tests+results from the given tests dictionary.
 
-  Parse individual tests and results from the builder tests dictionary,
-  and place into a flattened tests results dictionary. Most tests are
-  standalone, and keyed by thier test name. Some tests belong to a
-  'testinstance' group, and are keyed by their testinstance name, the
-  test data value (e.g., '0', '1', '2'), and the test name.
+  Parse individual tests and results from the |tr_tests_dict|, and place them
+  into a flattened tests results dictionary. Most tests are standalone, and
+  keyed by their test name. Some tests belong to a testinstance group, and are
+  keyed by their testinstance group name, then the testinstance number
+  (e.g., '0', '1', '2'), and finally the test name.
 
   For example, a standalone test:result is formatted thus:
   "BookmarksTest.CommandOpensBookmarksTab": {
@@ -255,32 +585,39 @@
   }
 
   Tests grouped under a testinstance, are formatted thus:
-  "MediaStreamDevicesControllerBrowserTestInstance": {
-    "MediaStreamDevicesControllerBrowserTest.AudioCaptureAllowed": {
+  "KioskUpdateSuite": {
+    "KioskUpdateTest.PermissionChange": {
       "1": {
         "results": [...],
         "times": [...]
       }
-    "MediaStreamDevicesControllerBrowserTest.VideoCaptureAllowed": {
+    "KioskUpdateTest.PermissionChange": {
       "0": {
         "results": [...],
         "times": [...]
       }
     }
 
+  The flattened test results dictionary is formatted thus:
+    {
+      BookmarksTest.CommandOpensBookmarksTab: [[60, u'Q'], [440, u'P']],
+      KioskUpdateTest.PermissionChange/0: [[498, u'P'], [2, u'Q']]
+      KioskUpdateTest.PermissionChange/1: [[493, u'P'], [7, u'Q']]
+    }
+
   Args:
-    builder_tests_dict: Dictionary of test groups & tests on builder.
+    tr_tests_dict: Dictionary of test groups & tests.
 
   Returns:
-    Dictionary of flattened tests and their results on builder.
+    Dictionary of flattened tests and their results.
   """
   tests_results_dict = {}
   standalone = 0
   group = 0
   subtest = 0
 
-  for group_name in builder_tests_dict:
-    test_group = builder_tests_dict[group_name]
+  for group_name in tr_tests_dict:
+    test_group = tr_tests_dict[group_name]
     if '.' in group_name:
       standalone += 1
       test_result = test_group['results']
@@ -295,39 +632,49 @@
           long_test_name = '%s/%s/%s' % (group_name, test_name, value)
           tests_results_dict[long_test_name] = test_result
 
-  print '  Number of standalone tests: %s' % standalone
-  print '  Number of instance tests (in %s groups): %s' % (group, subtest)
-  print '  Total tests results: %s\n' % len(tests_results_dict)
+  print('  Number of standalone tests: %s' % standalone)
+  print('  Number of instance tests (in %s groups): %s' % (group, subtest))
+  print('  Total tests results: %s\n' % len(tests_results_dict))
 
   return tests_results_dict
 
 
-def _CreateUserTestsResultsDict(test_results_dict, run_user_tests):
-  """Create dictionary of tests:results for all user-specified tests.
+def _CreateUserTestsResults(run_user_tests, stdio_tests_dict,
+                            tests_results_dict):
+  """Create dictionary of tests results for all user-specified tests.
 
-  If a user specified test is missing from builder test+results, then default
-  the test result to the code for missing test: 'O'.
+  If a user test is failed in the build status given by |stdio_tests_dict|,
+  then set the test result to failed code: 'Q'. If a user test is in the test
+  results given by |test_results_dict|, then use those results. Otherwise,
+  use the test result given by |stdio_tests_dict|. If a user test is missing
+  from both |stdio_tests_dict| and |test_results_dict|, then set the test
+  test result to missing code: 'O'.
 
   Args:
-    test_results_dict: Builder tests+results dictionary
     run_user_tests: List of run instances of user specified tests.
+    stdio_tests_dict: builder's results.json test results.
+    tests_results_dict: test results from the tests-results server.
 
   Returns:
-    Dictionary of tests to results for all user specified tests.
+    Dictionary of tests and results for all user specified tests.
   """
   user_tests_results_dict = {}
-  # Iterate over run user-specified tests.
+  # Iterate over tests in the run user-specified tests list.
   for test_name in run_user_tests:
-    if test_name in test_results_dict:  # Test has results.
-      test_result = test_results_dict[test_name]
+    if (test_name in stdio_tests_dict and
+        stdio_tests_dict[test_name] == [[999, u'Q']]):
+      test_result = stdio_tests_dict[test_name]
+    elif test_name in tests_results_dict:  # Use test-results server results.
+      test_result = tests_results_dict[test_name]
+    elif test_name in stdio_tests_dict:  # Use builder results.json results.
+      test_result = stdio_tests_dict[test_name]
     else:
       test_result = [[999, u'O']]  # Set result to missing.
     user_tests_results_dict[test_name] = test_result
-
   return user_tests_results_dict
 
 
-def _CreateResultOfTestsDict(user_tests_results_dict):
+def _CreateResultOfTests(user_tests_results_dict):
   """Create dictionary of user tests keyed by result.
 
   Args:
@@ -357,13 +704,14 @@
           _MISSING: missing_tests}
 
 
-def _ReportTestsByResult(result_of_tests_dict, tr_dict, report_dir):
+def _ReportTestsByResult(result_of_tests_dict, tr_dict, rout, rdir):
   """Print and write report of tests, grouped by result type.
 
   Args:
     result_of_tests_dict: Dictionary of results for tests.
     tr_dict: Dictionary of tests to results for builder.
-    report_dir: Directory where to save report.
+    rout: flag whether to write report file.
+    rdir: Directory where to write report.
   """
   # Test report result types and section headers.
   report_results_headers = {
@@ -374,122 +722,212 @@
   }
   report_section_order = [_NOTRUN, _FAILED, _PASSED, _MISSING]
 
-  ofile = open(report_dir+'/report', 'w')
+  if rout:
+    ofile = open(rdir+'/report', 'w')
   for result_type in report_section_order:
     header = report_results_headers[result_type]
     tests = result_of_tests_dict[result_type]
-    print '%s (%s)' % (header, len(tests))
-    ofile.write('%s (%s)\n' % (header, len(tests)))
-    for num, test_name in enumerate(sorted(tests)):
-      if test_name in tr_dict:
-        print '  %s) %s: %s' % (num+1, test_name, tr_dict[test_name][0:2])
-        ofile.write('  %s) %s: %s\n' % (num+1, test_name, tr_dict[test_name]))
+    print('%s (%s)' % (header, len(tests)))
+    if rout:
+      ofile.write('%s (%s)\n' % (header, len(tests)))
+    for num, test in enumerate(sorted(tests)):
+      if test in tr_dict:
+        print('  %s) %s: %s' % (num+1, test, tr_dict[test][0:2]))
+        if rout:
+          ofile.write('  %s) %s: %s\n' % (num+1, test, tr_dict[test]))
       else:
-        print '  %s) %s' % (num+1, test_name)
-        ofile.write('  %s) %s\n' % (num+1, test_name))
-  ofile.close()
+        print('  %s) %s' % (num+1, test))
+        if rout:
+          ofile.write('  %s) %s\n' % (num+1, test))
+  if rout:
+    ofile.close()
+
+
+def _SetUrlHost(url, host):
+  """Modify builder |url| to point to the the correct |host|.
+
+  Args:
+    url: Builder URL, which may not have the correct host.
+    host: Correct builder host.
+
+  Returns:
+    Builder URL with the correct host.
+  """
+  pattern = '(?:http.?://)?(?P<host>[^:/ ]+).*'
+  m = re.search(pattern, url)
+  original_host = m.group('host')
+  rehosted_url = re.sub(original_host, host, url)
+  return rehosted_url
 
 
 def main():
   """Report test results of specified browsertests."""
   parser = argparse.ArgumentParser(
       description=('Report run status and test results for a user-specified '
-                   'list of browser_test tests.'),
+                   'list of browsertest tests.'),
       formatter_class=argparse.RawDescriptionHelpFormatter)
   parser.add_argument('--tests_file', dest='tests_file', default=_TESTS_FILE,
                       help=('Specify tests path/file '
                             '(default is %s).' % _TESTS_FILE))
+  parser.add_argument('--report_out', dest='report_out', default=None,
+                      help=('Write report (default is None).'))
   parser.add_argument('--report_dir', dest='report_dir', default=_REPORT_DIR,
                       help=('Specify path to report directory '
                             '(default is %s).' % _REPORT_DIR))
   parser.add_argument('--master', dest='master', default=_TR_MASTER,
                       help=('Specify build master repository '
                             '(default is %s).' % _TR_MASTER))
-  parser.add_argument('--builder', dest='builder', default=_TR_BUILDER,
-                      help=('Specify test builder '
-                            '(default is %s).' % _TR_BUILDER))
-  parser.add_argument('--build_num', dest='build_num', default=_BUILD_NUMBER,
-                      help=('Specify builder build number '
+  parser.add_argument('--builder_host', dest='builder_host',
+                      default=_BUILDER_HOST,
+                      help=('Specify builder host name '
+                            '(default is %s).' % _BUILDER_HOST))
+  parser.add_argument('--builder_project', dest='builder_project',
+                      default=_BUILDER_PROJECT,
+                      help=('Specify builder project name '
+                            '(default is %s).' % _BUILDER_PROJECT))
+  parser.add_argument('--builder_name', dest='builder_name',
+                      default=_BUILDER_NAME,
+                      help=('Specify builder name '
+                            '(default is %s).' % _BUILDER_NAME))
+  parser.add_argument('--build_num', dest='build_num', type=int,
+                      default=_BUILD_NUMBER,
+                      help=('Specify positive build number, or negative '
+                            'index from latest build '
                             '(default is %s).' % _BUILD_NUMBER))
   parser.add_argument('--test_type', dest='test_type', default=_TEST_TYPE,
-                      help=('Specify test type '
+                      help=('Specify test type: browser_tests or '
+                            'interactive_ui_tests '
                             '(default is %s).' % _TEST_TYPE))
+  parser.add_argument('--version', dest='cr_version', default=None,
+                      help=('Specify chromium version number '
+                            '(default is None).'))
   parser.add_argument('--print_types', dest='print_types',
                       action='store_true', help='Print test result types.')
   arguments = parser.parse_args()
 
+  ### Set parameters from CLI arguments, and check for valid values.
   # Set parameters from command line arguments.
   tests_file = arguments.tests_file
+  report_out = arguments.report_out
   report_dir = arguments.report_dir
   master = arguments.master
-  builder = arguments.builder
+  builder_host = arguments.builder_host
+  builder_proj = arguments.builder_project
+  builder_name = arguments.builder_name
   build_num = arguments.build_num
   test_type = arguments.test_type
+  cr_version = arguments.cr_version
   print_types = arguments.print_types
 
-  # Print map of test result types.
+  # Print map of test result types and exit.
   if print_types:
-    print 'Test result types:'
-    print json.dumps(_RESULT_TYPES, indent=4)
-    sys.exit()
+    print('Test result types:')
+    print(json.dumps(_RESULT_TYPES, indent=4))
+    sys.exit(0)
 
-  # Ensure default or user defined tests file points to a real file.
+  # Ensure default or user-defined |tests_file| points to a real file.
   if not os.path.isfile(tests_file):
-    print 'Error: Could not find tests file. Try passing in --tests_file.'
+    print('Error: Could not find tests file. Try passing in --tests_file.')
     sys.exit(2)
 
-  # Ensure default or user-defined report folder points to a real dir.
+  # Ensure default or user-defined |report_dir| points to a real directory.
   if not os.path.exists(report_dir):
-    print ('Error: Could not find report directory. '
-           'Try passing in --report_dir.')
+    print(('Error: Could not find report directory. '
+           'Try passing in --report_dir.'))
     sys.exit(2)
 
-  # Get builder stdio log test info for build number.
-  # Find latest completed build number.
-  if build_num == '0' or build_num == '-1':
-    print ('Error: Invalid build number: %s. '
-           'Using %s instead.' % (build_num, _BUILD_NUMBER))
-    build_num = _BUILD_NUMBER
-  completed_build_num = _FindLatestCompletedBuildNumber(build_num)
+  # Verify that user gave |build_num| or |cr_version|, but not both.
+  if build_num != _BUILD_NUMBER and cr_version:
+    print(('Error: You may specify the build_num or the cr_version, '
+           'but not both.'))
+    sys.exit(2)
 
-  # Get list of test instances run on builder for build number.
-  stdio_log_url = _GetStdioLogUrl(builder, completed_build_num, test_type)
-  stdio_tests_dict = _GetStdioLogTestsDict(stdio_log_url)
+  # Verify user gave valid |test_type|.
+  if test_type not in ['browser_tests', 'interactive_ui_tests']:
+    print(('Error: Invalid test_type: %s. Use \'browser_tests\' or '
+           '\'interactive_ui_tests\'') % test_type)
+    sys.exit(2)
 
-  # Read list of user-specified tests from tests file.
+  # Pack valid build info into a portable dictionary.
+  build_dict = {
+      'builder_host': builder_host,
+      'builder_proj': builder_proj,
+      'builder_name': builder_name,
+      'build_num': build_num,
+      'test_type': test_type,
+      'cr_version': cr_version
+  }
+
+  ### Get list of user tests from |tests_file|.
   user_tests = _GetUserSpecifiedTests(tests_file)
-  if not user_tests:
-    print 'Error: tests file is empty.'
-    sys.exit(2)
-  print '  Number of user tests: %s' % len(user_tests)
 
-  # Get list of instances of run and not run user tests.
+  ### Determine build number from which to get status.
+  # Get list of available builds from builder.
+  builds_list = _GetBuildsList(build_dict)
+
+  # Find the latest completed build by chromium version.
+  if cr_version:
+    build_num = _FindBuildByChromiumVersion(build_dict)
+
+  # Set nominal build_num from builds available in |builds_list|.
+  build_dict['build_num'] = _GetNominalBuildNumber(build_num, builds_list)
+
+  # Get number of latest completed build, and update build_dict with it.
+  build_num, build_test_status_dict = (
+      _LatestCompletedBuild(build_dict, builds_list))
+  build_dict['build_num'] = build_num
+
+  ### Get build status from the builder for the build number.
+  # Get the build status of the latest completed build.
+  build_status_dict = _GetBuildStatus(build_dict)
+
+  # Extract the build properties, and print chromium version.
+  build_properties = build_status_dict['properties']
+  _PrintChromiumVersion(build_properties)
+
+  ### Get test status from the builder for the build number.
+  # Get list of failed tests from build status.
+  tests_failed_list = _GetTestsFailedList(build_dict, build_status_dict)
+
+  # Get stdio log URL from build test status for the latest build_num.
+  stdio_log_url = (
+      _GetStdioLogUrlFromBuildStatus(build_dict, build_test_status_dict))
+
+  # Get dictionary of test instances run on selected build.
+  stdio_tests_dict = _GetStdioLogTests(stdio_log_url, tests_failed_list)
+
+  # Get instances of run and not run user tests.
   run_user_tests, notrun_user_tests = (
       _RunAndNotrunTests(stdio_tests_dict, user_tests))
 
-  # Get run user tests and results data from the specified builder.
-  if run_user_tests:
-    # Fetch builder results dictionary from test-results server.
-    builder_results_dict = _GetResultsDict(master, builder, test_type)
-    builder_tests_dict = builder_results_dict['tests']
+  ### Read test results from test-results server for the builder.
+  test_results_json = _GetTestResultsJson(master, builder_name, test_type)
+  if test_results_json:
+    # Extract tests results dictionary from results json for builder.
+    tr_tests_dict = json.loads(test_results_json)[builder_name]['tests']
+    tests_results_dict = _CreateTestsResultsDictionary(tr_tests_dict)
   else:
-    builder_tests_dict = {}
+    # Extract test results from stdio tests dictionary.
+    tests_results_dict = stdio_tests_dict
 
-  # Extract tests to results dictionary from builder tests dictionary.
-  tests_results_dict = _CreateTestsResultsDictionary(builder_tests_dict)
-
+  ### Combine run user tests, build test status, and test results into a
+  ### single dictionary of user tests and their results, and then into a
+  ### dictionary of results and their tests.
   # Create dictionary of run user test instances and results.
   user_tests_results_dict = (
-      _CreateUserTestsResultsDict(tests_results_dict, run_user_tests))
+      _CreateUserTestsResults(run_user_tests, stdio_tests_dict,
+                              tests_results_dict))
 
   # Create dictionary of run tests that are failed, passed, and missing.
-  result_of_tests_dict = _CreateResultOfTestsDict(user_tests_results_dict)
+  result_of_tests_dict = _CreateResultOfTests(user_tests_results_dict)
 
   # Add list of not run tests to the result of tests dictionary.
   result_of_tests_dict[_NOTRUN] = notrun_user_tests
 
-  # Output tests by result type: notrun, failed, passed, and missing.
-  _ReportTestsByResult(result_of_tests_dict, tests_results_dict, report_dir)
+  ### Output report of tests grouped by result. Result types are notrun,
+  ### failed, passed, and missing
+  _ReportTestsByResult(result_of_tests_dict, tests_results_dict,
+                       report_out, report_dir)
 
 if __name__ == '__main__':
   main()