gce_au_worker: Avoid using colon in file names

The `test_that` script uses colon as a seperator in autotest suite names, e.g.,
`test_that -b lakitu 1.1.1.1 suite:gce-smoke`. We used to use the full suite
name as file name when saving test results, and it confused a piece of chromite
code (chromite.commands.ListFailedTests, which uses a regular expression to
match file names and colon is not deemed as a valid character) that parses test
logs for failed tests.

A sample failure can be found here (notice the 'IndexError' at the bottom of
the log):
https://uberchromegw.corp.google.com/i/chromeos/builders/lakitu%20canary/builds/712/steps/GCETest%20%28attempt%202%29/logs/stdio

While we can change the chromite code to include colon, I think it's better to
not use colon in file names at all, as it is commonly used as seperator in
things like $PATH and URL.

This CL also tweaks VerifyImage to print out the summarized test report in case
of a test failure, and includes some style smashes to better conform with the
style guide.

BUG=b:25849966
TEST=Unit test. Also ran a trybot against lakitu-pre-cq, with an intentional
    test failure and verified that failed test was correctly linked in the
    buildbot log, and the 'IndexError' was gone.

Change-Id: Ie4b3b002c95b81f30c4c63799b77819651050757
Reviewed-on: https://chromium-review.googlesource.com/317781
Commit-Ready: Daniel Wang <wonderfly@google.com>
Tested-by: Daniel Wang <wonderfly@google.com>
Reviewed-by: Amey Deshpande <ameyd@google.com>
diff --git a/au_test_harness/gce_au_worker.py b/au_test_harness/gce_au_worker.py
index 1789ca1..266f74d 100644
--- a/au_test_harness/gce_au_worker.py
+++ b/au_test_harness/gce_au_worker.py
@@ -102,10 +102,11 @@
       Background processes that delete stale instances and images.
   """
 
-  GS_PATH_COMMON_PREFIX = 'gs://'
-  GS_URL_COMMON_PREFIX = 'https://storage.googleapis.com/'
-  IMAGE_PREFIX = 'test-image-'
-  INSTANCE_PREFIX = 'test-instance-'
+  _GS_PATH_COMMON_PREFIX = 'gs://'
+  _GS_URL_COMMON_PREFIX = 'https://storage.googleapis.com/'
+  _IMAGE_PREFIX = 'test-image-'
+  _INSTANCE_PREFIX = 'test-instance-'
+  _TEST_REPORT_FILENAME = 'test_report.log'
 
   def __init__(self, options, test_results_root,
                project=constants.GCE_PROJECT,
@@ -194,18 +195,18 @@
     return_values = parallel.RunParallelSteps(steps, return_values=True)
 
     passed = True
-    outputs = {}
-    for test, percent_passed, output in return_values:
+    test_reports = {}
+    for test, percent_passed, report in return_values:
       passed &= (percent_passed == 100)
-      outputs[test] = output
+      test_reports[test] = report
 
     if not passed:
       self._HandleFail(log_directory_base, fail_directory_base)
+      print ('\nSome test(s) failed. Test reports:')
+      for test, report in test_reports.iteritems():
+        print ('\nTest: %s\n%s' % (test, report or ''))
       if unittest is not None:
-        unittest.fail('Not all tests passed')
-      for test, output in outputs.iteritems():
-        print ('\nTest: %s\n' % test)
-        print (output)
+        unittest.fail('Not all tests passed.')
     return passed
 
   # --- PRIVATE HELPER FUNCTIONS ---
@@ -217,20 +218,18 @@
     'test_that'.
 
     Args:
-      test: (str) The test or suite to run.
-      remote: (str) The hostname of the remote DUT.
-      log_directory_base:
-          (str) The base directory to store test logs. A sub directory specific
-          to this test will be created there.
-      fail_directory_base:
-          (str) The base directory to store test logs in case of a test failure.
+      test: The test or suite to run.
+      remote: The hostname of the remote DUT.
+      log_directory_base: The base directory to store test logs. A sub directory
+          specific to this test will be created there.
+      fail_directory_base: The base directory to store test logs in case of a
+          test failure.
 
     Returns:
-      test:
-          (str) Same as |test|. This is useful when the caller wants to
-          correlate results to the test name.
-      percent_passed: (int) Pass rate.
-      output: (str): Original test output.
+      test: Same as |test|. This is useful when the caller wants to correlate
+          results to the test name.
+      percent_passed: Pass rate.
+      test_report: Content of the test report generated by test_that.
     """
     log_directory, _ = self._GetResultsDirectoryForTest(
         test, log_directory_base, fail_directory_base)
@@ -258,21 +257,44 @@
                                          redirect_stdout=True,
                                          cwd=constants.CROSUTILS_DIR)
       percent_passed = self.ParseGeneratedTestOutput(result.output)
-      return test, percent_passed, result.output
+      test_report = self._GetTestReport(log_directory)
+
+      # Returns the summarized test_report as it is more useful than the full
+      # output, plus the entire log will always be linked in the failure report.
+      return test, percent_passed, test_report
+
+  def _GetTestReport(self, results_path):
+    """Returns the content of test_report.log created by test_that.
+
+    Args:
+      results_path: Path to the directory where results are saved.
+
+    Returns:
+      Content of test_report.log, or None if report is not found.
+    """
+    report_path = os.path.join(results_path, self._TEST_REPORT_FILENAME)
+    if os.path.isfile(report_path):
+      with open(report_path) as f:
+        return f.read()
+    logging.warning('Test log not found in %s', results_path)
+    return None
 
   def _GetResultsDirectoryForTest(self, test, log_directory_base,
                                   fail_directory_base):
     """Gets the log and fail directories for a particular test.
 
     Args:
-      test: (str) The test or suite to get directories for.
-      log_directory_base:
-          (str) The base directory where all test results are saved.
-      fail_directory_base:
-          (str) The base directory where all test failures are recorded.
+      test: The test or suite to get directories for.
+      log_directory_base: The base directory where all test results are saved.
+      fail_directory_base: The base directory where all test failures are
+          recorded.
     """
-    log_directory = os.path.join(log_directory_base, test)
-    fail_directory = os.path.join(fail_directory_base, test)
+    # Avoid using colon in file names. Not that it's not allowed, but it causes
+    # confusions and inconvenience as it is used as a separator in many cases,
+    # e.g., $PATH and url.
+    sanitized_test_name = test.replace(':', '_')
+    log_directory = os.path.join(log_directory_base, sanitized_test_name)
+    fail_directory = os.path.join(fail_directory_base, sanitized_test_name)
 
     if not os.path.exists(log_directory):
       os.makedirs(log_directory)
@@ -372,7 +394,7 @@
           'Error: %s' % e)
 
     # Create an image from |image_path| and an instance from the image.
-    image = '%s%s' % (self.IMAGE_PREFIX, ts)
+    image = '%s%s' % (self._IMAGE_PREFIX, ts)
     try:
       self.image_link = self.gce_context.CreateImage(
           image, self._GsPathToUrl(self.tarball_remote))
@@ -393,7 +415,7 @@
     for test in self.tests:
       ts = datetime.datetime.fromtimestamp(time.time()).strftime(
           '%Y-%m-%d-%H-%M-%S')
-      instance = '%s%s' % (self.INSTANCE_PREFIX, ts)
+      instance = '%s%s' % (self._INSTANCE_PREFIX, ts)
       kwargs = test['flags'].copy()
       kwargs['description'] = 'For test %s' % test['name']
       steps.append(partial(self._CreateInstance, instance,
@@ -409,13 +431,11 @@
     and deletes it on true.
 
     Args:
-      resource: (str) The resource name/url to delete.
-      existence_checker:
-        (callable) The callable to check existence. This callable should take
-        |resource| as its first argument.
-      deletor:
-        (callable) The callable to perform the deletion. This callable should
-        take |resource| as its first argument.
+      resource: The resource name/url to delete.
+      existence_checker: A callable to check existence. This callable should
+          take |resource| as its first argument.
+      deletor: A callable to perform the deletion. This callable should take
+          |resource| as its first argument.
 
     Raises:
       ValueError if existence_checker or deletor is not callable.
@@ -508,10 +528,10 @@
     Raises:
       ValueError if |gs_path| is not a valid GS path.
     """
-    if not gs_path.startswith(self.GS_PATH_COMMON_PREFIX):
+    if not gs_path.startswith(self._GS_PATH_COMMON_PREFIX):
       raise ValueError('Invalid GCS path: %s' % gs_path)
-    return gs_path.replace(self.GS_PATH_COMMON_PREFIX,
-                           self.GS_URL_COMMON_PREFIX, 1)
+    return gs_path.replace(self._GS_PATH_COMMON_PREFIX,
+                           self._GS_URL_COMMON_PREFIX, 1)
 
   def _WaitForBackgroundDeleteProcesses(self):
     """Waits for all background proecesses to finish."""
diff --git a/au_test_harness/gce_au_worker_unittest.py b/au_test_harness/gce_au_worker_unittest.py
index 3820a71..e88de00 100755
--- a/au_test_harness/gce_au_worker_unittest.py
+++ b/au_test_harness/gce_au_worker_unittest.py
@@ -200,7 +200,7 @@
     ]
     actual_tests_run = []
 
-    def _OverrideGetInstanceIP(instance, *unused_args, **unused_kwargs):
+    def _OverrideGetInstanceIP(instance, *_args, **_kwargs):
       if instance == 'instance_1':
         return '1.1.1.1'
       elif instance == 'instance_2':
@@ -208,7 +208,7 @@
       else:
         return '3.3.3.3'
 
-    def _OverrideRunCommand(cmd, *unused_args, **unused_kwargs):
+    def _OverrideRunCommand(cmd, *_args, **_kwargs):
       """A mock of cros_build_lib.RunCommand that injects arguments."""
       # In this test setup, |test| and |remote| should be the third and fourth
       # last argument respectively.
@@ -217,7 +217,7 @@
       actual_tests_run.append(dict(remote=remote, test=test))
       return cros_build_lib.CommandResult()
 
-    def _OverrideRunParallelSteps(steps, *unused_args, **unused_kwargs):
+    def _OverrideRunParallelSteps(steps, *_args, **_kwargs):
       """Run steps sequentially."""
       return_values = []
       for step in steps: