[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)