[autotest] Puppy lab testing environment.

This is the first commit for a more reliable testing
environment for ChromeOS lab infrastructure. It creates
a class capable of mocking out job results without
turning the parsing operation into a no-op. This allows
us to schedule tests against fake DUTs without actually
needing to ssh-into them, which is pretty much all that
dummy_Pass actually does.

The cl also sneaks in a fix to hide the exception stack trace
from run_pylint.

TEST=Ran bvt.
BUG=chromium:423225,chromium:425347

Change-Id: I5ecbcf1ff08bf9f45af26333c2ccb4c4022a97d6
Reviewed-on: https://chromium-review.googlesource.com/227783
Reviewed-by: Prashanth B <beeps@chromium.org>
Commit-Queue: Prashanth B <beeps@chromium.org>
Tested-by: Prashanth B <beeps@chromium.org>
diff --git a/client/common_lib/time_utils.py b/client/common_lib/time_utils.py
index 695d348..57eb866 100644
--- a/client/common_lib/time_utils.py
+++ b/client/common_lib/time_utils.py
@@ -42,16 +42,19 @@
     return time.mktime(time.strptime(date_string, TIME_FMT))
 
 
-def epoch_time_to_date_string(epoch_time):
+def epoch_time_to_date_string(epoch_time, fmt_string=TIME_FMT):
     """Convert epoch time (float) to a human readable date string.
 
     @param epoch_time The number of seconds since the UNIX epoch, as
                       a float.
+    @param fmt_string: A string describing the format of the datetime
+        string output.
+
     @returns: string formatted in the following way: "yyyy-mm-dd hh:mm:ss"
     """
     if epoch_time:
         return datetime.datetime.fromtimestamp(
-                int(epoch_time)).strftime(TIME_FMT)
+                int(epoch_time)).strftime(fmt_string)
     return None
 
 
diff --git a/puppylab/__init__.py b/puppylab/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/puppylab/__init__.py
diff --git a/puppylab/common.py b/puppylab/common.py
new file mode 100644
index 0000000..9941b19
--- /dev/null
+++ b/puppylab/common.py
@@ -0,0 +1,8 @@
+import os, sys
+dirname = os.path.dirname(sys.modules[__name__].__file__)
+autotest_dir = os.path.abspath(os.path.join(dirname, ".."))
+client_dir = os.path.join(autotest_dir, "client")
+sys.path.insert(0, client_dir)
+import setup_modules
+sys.path.pop(0)
+setup_modules.setup(base_path=autotest_dir, root_module_name="autotest_lib")
diff --git a/puppylab/results_mocker.py b/puppylab/results_mocker.py
new file mode 100644
index 0000000..2eabbbb
--- /dev/null
+++ b/puppylab/results_mocker.py
@@ -0,0 +1,133 @@
+# Copyright (c) 2014 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.
+
+"""Mock out test results for puppylab.
+"""
+
+
+import logging
+import os
+import time
+
+import common
+from autotest_lib.client.common_lib import time_utils
+from autotest_lib.puppylab import templates
+
+
+class ResultsMocker(object):
+    """Class to mock out the results of a test."""
+
+    def _make_dirs(self):
+        """Create essential directories needed for faking test results.
+
+        @raises ValueError: If the directories crucial to reporting
+            test status already exist.
+        @raises OSError: If we cannot make one of the directories for
+            an os related reason (eg: permissions).
+        @raises AssertionError: If one of the directories silently failed
+            creation.
+        """
+        logging.info("creating dir %s, %s, %s",
+                self.results_dir, self.test_results, self.host_keyval_dir)
+        if not os.path.exists(self.results_dir):
+            os.makedirs(self.results_dir)
+        if not os.path.exists(self.test_results):
+            os.makedirs(self.test_results)
+        if not os.path.exists(self.host_keyval_dir):
+            os.makedirs(self.host_keyval_dir)
+        assert(os.path.exists(self.test_results) and
+               os.path.exists(self.results_dir) and
+               os.path.exists(self.host_keyval_dir))
+
+
+    def __init__(self, test_name, results_dir, machine_name):
+        """Initialize a results mocker.
+
+        @param test_name: The name of the test, eg: dummy_Pass.
+        @param results_dir: The results directory this test will use.
+        @param machine_name: A string representing the hostname the test will
+            run on.
+        """
+        self.results_dir = results_dir
+        self.test_results = os.path.join(results_dir, test_name)
+        self.host_keyval_dir = os.path.join(self.results_dir, 'host_keyvals')
+        self.machine_name = machine_name
+        self.test_name = test_name
+
+        self._make_dirs()
+
+        # Status logs are used by the parser to declare a test as pass/fail.
+        self.job_status = os.path.join(self.results_dir, 'status')
+        self.job_status_log = os.path.join(self.results_dir, 'status.log')
+        self.test_status = os.path.join(self.test_results, 'status')
+
+        # keyvals are used by the parser to figure out fine grained information
+        # about a test. Only job_keyvals are crucial to parsing.
+        self.test_keyvals = os.path.join(self.test_results, 'keyval')
+        self.job_keyvals = os.path.join(self.results_dir, 'keyval')
+        self.host_keyvals = os.path.join(self.results_dir, machine_name)
+
+
+    def _write(self, results_path, results):
+        """Write the content in results to the file in results_path.
+
+        @param results_path: The path to the results file.
+        @param results: The content to write to the file.
+        """
+        logging.info('Writing results to %s', results_path)
+        with open(results_path, 'w') as results_file:
+            results_file.write(results)
+
+
+    def generate_keyvals(self):
+        """Apply templates to keyval files.
+
+        There are 3 important keyvals files, only one of which is actually
+        crucial to results parsing:
+            host_keyvals - information about the DUT
+            job_keyvals - information about the server_job
+            test_keyvals - information about the test
+
+        Parsing cannot complete without the job_keyvals. Everything else is
+        optional. Keyvals are parsed into tko tables.
+        """
+        #TODO(beeps): Include other keyvals.
+        self._write(
+                self.job_keyvals,
+                templates.job_keyvals_template %
+                        {'hostname': self.machine_name})
+
+
+    def generate_status(self):
+        """Generate status logs.
+
+        3 important status logs are required for successful parsing:
+            test_name/status - core test status
+            results_dir/status - server job status (has test status in it)
+            status.log - compiled final status log
+        """
+        current_timestamp = int(time.time())
+        test_info = {
+            'test_name': self.test_name,
+            'timestamp': current_timestamp,
+            'date': time_utils.epoch_time_to_date_string(
+                            current_timestamp, fmt_string='%b %d %H:%M:%S'),
+        }
+        self._write(
+                self.job_status,
+                templates.success_job_template % test_info)
+        self._write(
+                self.job_status_log,
+                templates.success_job_template % test_info)
+        self._write(
+                self.test_status,
+                templates.success_test_template % test_info)
+
+
+    def mock_results(self):
+        """Create mock results in the directories used to init the instance."""
+        self.generate_status()
+        self.generate_keyvals()
+
+
diff --git a/puppylab/templates.py b/puppylab/templates.py
new file mode 100644
index 0000000..5ff0d01
--- /dev/null
+++ b/puppylab/templates.py
@@ -0,0 +1,40 @@
+# Copyright (c) 2014 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.
+
+"""Templates for results a test can emit.
+"""
+
+# TODO(beeps): The right way to create these status logs is by creating a job
+# object and invoking job.record on it. However we perform this template
+# hack instead for the following reasons:
+#   * The templates are a lot easier to understand at first glance, which
+#     is really what one wants from a testing interface built for a
+#     framework like autotest.
+#   * Creating the job is wedged into core autotest code, so it has
+#     unintended consequences like checking for hosts/labels etc.
+#   * We are guaranteed to create the bare minimum by hand specifying the file
+#     to write, and their contents. Job.record ends up checking and creating
+#     several non-essential directoris in the process or recording status.
+
+success_test_template = (
+        "\tSTART\t%(test_name)s\t%(test_name)s"
+        "\ttimestamp=%(timestamp)s\tlocaltime=%(date)s\n"
+        "\t\tGOOD\t%(test_name)s\t%(test_name)s\ttimestamp=%(timestamp)s\t"
+        "localtime=%(date)s\tcompleted successfully\n"
+        "\tEND GOOD\t%(test_name)s\t%(test_name)s\ttimestamp=%(timestamp)s\t"
+        "localtime=%(date)s")
+
+
+success_job_template = (
+        "START\t----\t----\ttimestamp=%(timestamp)s\tlocaltime=%(date)s"
+        "\n\tSTART\t%(test_name)s\t%(test_name)s\ttimestamp=%(timestamp)s\t"
+        "localtime=%(date)s\n\t\tGOOD\t%(test_name)s\t%(test_name)s"
+        "\ttimestamp=%(timestamp)s\tlocaltime=%(date)s\tcompleted "
+        "successfully\n\tEND GOOD\t%(test_name)s\t%(test_name)s"
+        "\ttimestamp=%(timestamp)s\tlocaltime=%(date)s\n"
+        "END GOOD\t----\t----\ttimestamp=%(timestamp)s\tlocaltime=%(date)s")
+
+
+job_keyvals_template = "hostname=%(hostname)s\nstatus_version=1\n"
+
diff --git a/utils/run_pylint.py b/utils/run_pylint.py
index c7429a1..c194352 100755
--- a/utils/run_pylint.py
+++ b/utils/run_pylint.py
@@ -10,7 +10,11 @@
 run_pylint.py filename.py
 """
 
-import fnmatch, os, re, sys
+import fnmatch
+import logging
+import os
+import re
+import sys
 
 import common
 from autotest_lib.client.common_lib import autotemp, revision_control
@@ -453,4 +457,8 @@
 
 
 if __name__ == '__main__':
-    main()
+    try:
+        main()
+    except Exception as e:
+        logging.error(e)
+        sys.exit(1)