| Index: client/common_lib/base_job.py
 | 
| diff --git a/client/common_lib/base_job.py b/client/common_lib/base_job.py
 | 
| index 3c77d386680a161a886e925428756761aba74939..c5f55f8d42f3816c8a8114e844a8c10a4f3d0b0f 100644
 | 
| --- a/client/common_lib/base_job.py
 | 
| +++ b/client/common_lib/base_job.py
 | 
| @@ -1,6 +1,6 @@
 | 
|  import os, copy, logging, errno, fcntl, time, re, weakref, traceback
 | 
| +import tarfile
 | 
|  import cPickle as pickle
 | 
| -
 | 
|  from autotest_lib.client.common_lib import autotemp, error, log
 | 
|  
 | 
|  
 | 
| @@ -422,6 +422,9 @@ class status_log_entry(object):
 | 
|      TIMESTAMP_FIELD = 'timestamp'
 | 
|      LOCALTIME_FIELD = 'localtime'
 | 
|  
 | 
| +    # non-space whitespace is forbidden in any fields
 | 
| +    BAD_CHAR_REGEX = re.compile(r'[\t\n\r\v\f]')
 | 
| +
 | 
|      def __init__(self, status_code, subdir, operation, message, fields,
 | 
|                   timestamp=None):
 | 
|          """Construct a status.log entry.
 | 
| @@ -439,18 +442,16 @@ class status_log_entry(object):
 | 
|  
 | 
|          @raise ValueError: if any of the parameters are invalid
 | 
|          """
 | 
| -        # non-space whitespace is forbidden in any fields
 | 
| -        bad_char_regex = r'[\t\n\r\v\f]'
 | 
|  
 | 
|          if not log.is_valid_status(status_code):
 | 
|              raise ValueError('status code %r is not valid' % status_code)
 | 
|          self.status_code = status_code
 | 
|  
 | 
| -        if subdir and re.search(bad_char_regex, subdir):
 | 
| +        if subdir and self.BAD_CHAR_REGEX.search(subdir):
 | 
|              raise ValueError('Invalid character in subdir string')
 | 
|          self.subdir = subdir
 | 
|  
 | 
| -        if operation and re.search(bad_char_regex, operation):
 | 
| +        if operation and self.BAD_CHAR_REGEX.search(operation):
 | 
|              raise ValueError('Invalid character in operation string')
 | 
|          self.operation = operation
 | 
|  
 | 
| @@ -460,7 +461,7 @@ class status_log_entry(object):
 | 
|          message_lines = message.split('\n')
 | 
|          self.message = message_lines[0].replace('\t', ' ' * 8)
 | 
|          self.extra_message_lines = message_lines[1:]
 | 
| -        if re.search(bad_char_regex, self.message):
 | 
| +        if self.BAD_CHAR_REGEX.search(self.message):
 | 
|              raise ValueError('Invalid character in message %r' % self.message)
 | 
|  
 | 
|          if not fields:
 | 
| @@ -468,7 +469,7 @@ class status_log_entry(object):
 | 
|          else:
 | 
|              self.fields = fields.copy()
 | 
|          for key, value in self.fields.iteritems():
 | 
| -            if re.search(bad_char_regex, key + value):
 | 
| +            if self.BAD_CHAR_REGEX.search(key + value):
 | 
|                  raise ValueError('Invalid character in %r=%r field'
 | 
|                                   % (key, value))
 | 
|  
 | 
| @@ -574,7 +575,8 @@ class status_logger(object):
 | 
|      @property subdir_filename: The filename to write subdir-level logs to.
 | 
|      """
 | 
|      def __init__(self, job, indenter, global_filename='status',
 | 
| -                 subdir_filename='status', record_hook=None):
 | 
| +                 subdir_filename='status', record_hook=None,
 | 
| +                 tap_writer=None):
 | 
|          """Construct a logger instance.
 | 
|  
 | 
|          @param job: A reference to the job object this is logging for. Only a
 | 
| @@ -589,12 +591,18 @@ class status_logger(object):
 | 
|          @param record_hook: An optional function to be called before an entry
 | 
|              is logged. The function should expect a single parameter, a
 | 
|              copy of the status_log_entry object.
 | 
| +        @param tap_writer: An instance of the class TAPReport for addionally
 | 
| +            writing TAP files
 | 
|          """
 | 
|          self._jobref = weakref.ref(job)
 | 
|          self._indenter = indenter
 | 
|          self.global_filename = global_filename
 | 
|          self.subdir_filename = subdir_filename
 | 
|          self._record_hook = record_hook
 | 
| +        if tap_writer is None:
 | 
| +            self._tap_writer = TAPReport(None)
 | 
| +        else:
 | 
| +            self._tap_writer = tap_writer
 | 
|  
 | 
|  
 | 
|      def render_entry(self, log_entry):
 | 
| @@ -647,6 +655,10 @@ class status_logger(object):
 | 
|              finally:
 | 
|                  fileobj.close()
 | 
|  
 | 
| +        # write to TAPRecord instance
 | 
| +        if log_entry.is_end() and self._tap_writer.do_tap_report:
 | 
| +            self._tap_writer.record(log_entry, self._indenter.indent, log_files)
 | 
| +
 | 
|          # adjust the indentation if this was a START or END entry
 | 
|          if log_entry.is_start():
 | 
|              self._indenter.increment()
 | 
| @@ -654,6 +666,191 @@ class status_logger(object):
 | 
|              self._indenter.decrement()
 | 
|  
 | 
|  
 | 
| +class TAPReport(object):
 | 
| +    """
 | 
| +    Deal with TAP reporting for the Autotest client.
 | 
| +    """
 | 
| +
 | 
| +    job_statuses = {
 | 
| +        "TEST_NA": False,
 | 
| +        "ABORT": False,
 | 
| +        "ERROR": False,
 | 
| +        "FAIL": False,
 | 
| +        "WARN": False,
 | 
| +        "GOOD": True,
 | 
| +        "START": True,
 | 
| +        "END GOOD": True,
 | 
| +        "ALERT": False,
 | 
| +        "RUNNING": False,
 | 
| +        "NOSTATUS": False
 | 
| +    }
 | 
| +
 | 
| +
 | 
| +    def __init__(self, enable, resultdir=None, global_filename='status'):
 | 
| +        """
 | 
| +        @param enable: Set self.do_tap_report to trigger TAP reporting.
 | 
| +        @param resultdir: Path where the TAP report files will be written.
 | 
| +        @param global_filename: File name of the status files .tap extensions
 | 
| +                will be appended.
 | 
| +        """
 | 
| +        self.do_tap_report = enable
 | 
| +        if resultdir is not None:
 | 
| +            self.resultdir = os.path.abspath(resultdir)
 | 
| +        self._reports_container = {}
 | 
| +        self._keyval_container = {} # {'path1': [entries],}
 | 
| +        self.global_filename = global_filename
 | 
| +
 | 
| +
 | 
| +    @classmethod
 | 
| +    def tap_ok(self, success, counter, message):
 | 
| +        """
 | 
| +        return a TAP message string.
 | 
| +
 | 
| +        @param success: True for positive message string.
 | 
| +        @param counter: number of TAP line in plan.
 | 
| +        @param message: additional message to report in TAP line.
 | 
| +        """
 | 
| +        if success:
 | 
| +            message = "ok %s - %s" % (counter, message)
 | 
| +        else:
 | 
| +            message = "not ok %s - %s" % (counter, message)
 | 
| +        return message
 | 
| +
 | 
| +
 | 
| +    def record(self, log_entry, indent, log_files):
 | 
| +        """
 | 
| +        Append a job-level status event to self._reports_container. All
 | 
| +        events will be written to TAP log files at the end of the test run.
 | 
| +        Otherwise, it's impossilble to determine the TAP plan.
 | 
| +
 | 
| +        @param log_entry: A string status code describing the type of status
 | 
| +                entry being recorded. It must pass log.is_valid_status to be
 | 
| +                considered valid.
 | 
| +        @param indent: Level of the log_entry to determine the operation if
 | 
| +                log_entry.operation is not given.
 | 
| +        @param log_files: List of full path of files the TAP report will be
 | 
| +                written to at the end of the test.
 | 
| +        """
 | 
| +        for log_file in log_files:
 | 
| +            log_file_path = os.path.dirname(log_file)
 | 
| +            key = log_file_path.split(self.resultdir, 1)[1].strip(os.sep)
 | 
| +            if not key:
 | 
| +                key = 'root'
 | 
| +
 | 
| +            if not self._reports_container.has_key(key):
 | 
| +                self._reports_container[key] = []
 | 
| +
 | 
| +            if log_entry.operation:
 | 
| +                operation = log_entry.operation
 | 
| +            elif indent == 1:
 | 
| +                operation = "job"
 | 
| +            else:
 | 
| +                operation = "unknown"
 | 
| +            entry = self.tap_ok(
 | 
| +                self.job_statuses.get(log_entry.status_code, False),
 | 
| +                len(self._reports_container[key]) + 1, operation + "\n"
 | 
| +            )
 | 
| +            self._reports_container[key].append(entry)
 | 
| +
 | 
| +
 | 
| +    def record_keyval(self, path, dictionary, type_tag=None):
 | 
| +        """
 | 
| +        Append a key-value pairs of dictionary to self._keyval_container in
 | 
| +        TAP format. Once finished write out the keyval.tap file to the file
 | 
| +        system.
 | 
| +
 | 
| +        If type_tag is None, then the key must be composed of alphanumeric
 | 
| +        characters (or dashes + underscores). However, if type-tag is not
 | 
| +        null then the keys must also have "{type_tag}" as a suffix. At
 | 
| +        the moment the only valid values of type_tag are "attr" and "perf".
 | 
| +
 | 
| +        @param path: The full path of the keyval.tap file to be created
 | 
| +        @param dictionary: The keys and values.
 | 
| +        @param type_tag: The type of the values
 | 
| +        """
 | 
| +        self._keyval_container.setdefault(path, [0, []])
 | 
| +        self._keyval_container[path][0] += 1
 | 
| +
 | 
| +        if type_tag is None:
 | 
| +            key_regex = re.compile(r'^[-\.\w]+$')
 | 
| +        else:
 | 
| +            if type_tag not in ('attr', 'perf'):
 | 
| +                raise ValueError('Invalid type tag: %s' % type_tag)
 | 
| +            escaped_tag = re.escape(type_tag)
 | 
| +            key_regex = re.compile(r'^[-\.\w]+\{%s\}$' % escaped_tag)
 | 
| +        self._keyval_container[path][1].extend([
 | 
| +            self.tap_ok(True, self._keyval_container[path][0], "results"),
 | 
| +            "\n  ---\n",
 | 
| +        ])
 | 
| +        try:
 | 
| +            for key in sorted(dictionary.keys()):
 | 
| +                if not key_regex.search(key):
 | 
| +                    raise ValueError('Invalid key: %s' % key)
 | 
| +                self._keyval_container[path][1].append(
 | 
| +                    '  %s: %s\n' % (key.replace('{', '_').rstrip('}'),
 | 
| +                                    dictionary[key])
 | 
| +                )
 | 
| +        finally:
 | 
| +            self._keyval_container[path][1].append("  ...\n")
 | 
| +        self._write_keyval()
 | 
| +
 | 
| +
 | 
| +    def _write_reports(self):
 | 
| +        """
 | 
| +        Write TAP reports to file.
 | 
| +        """
 | 
| +        for key in self._reports_container.keys():
 | 
| +            if key == 'root':
 | 
| +                sub_dir = ''
 | 
| +            else:
 | 
| +                sub_dir = key
 | 
| +            tap_fh = open(os.sep.join(
 | 
| +                [self.resultdir, sub_dir, self.global_filename]
 | 
| +            ) + ".tap", 'w')
 | 
| +            tap_fh.write('1..' + str(len(self._reports_container[key])) + '\n')
 | 
| +            tap_fh.writelines(self._reports_container[key])
 | 
| +            tap_fh.close()
 | 
| +
 | 
| +
 | 
| +    def _write_keyval(self):
 | 
| +        """
 | 
| +        Write the self._keyval_container key values to a file.
 | 
| +        """
 | 
| +        for path in self._keyval_container.keys():
 | 
| +            tap_fh = open(path + ".tap", 'w')
 | 
| +            tap_fh.write('1..' + str(self._keyval_container[path][0]) + '\n')
 | 
| +            tap_fh.writelines(self._keyval_container[path][1])
 | 
| +            tap_fh.close()
 | 
| +
 | 
| +
 | 
| +    def write(self):
 | 
| +        """
 | 
| +        Write the TAP reports to files.
 | 
| +        """
 | 
| +        self._write_reports()
 | 
| +
 | 
| +
 | 
| +    def _write_tap_archive(self):
 | 
| +        """
 | 
| +        Write a tar archive containing all the TAP files and
 | 
| +        a meta.yml containing the file names.
 | 
| +        """
 | 
| +        os.chdir(self.resultdir)
 | 
| +        tap_files = []
 | 
| +        for rel_path, d, files in os.walk('.'):
 | 
| +            tap_files.extend(["/".join(
 | 
| +                [rel_path, f]) for f in files if f.endswith('.tap')])
 | 
| +        meta_yaml = open('meta.yml', 'w')
 | 
| +        meta_yaml.write('file_order:\n')
 | 
| +        tap_tar = tarfile.open(self.resultdir + '/tap.tar.gz', 'w:gz')
 | 
| +        for f in tap_files:
 | 
| +            meta_yaml.write("  - " + f.lstrip('./') + "\n")
 | 
| +            tap_tar.add(f)
 | 
| +        meta_yaml.close()
 | 
| +        tap_tar.add('meta.yml')
 | 
| +        tap_tar.close()
 | 
| +
 | 
| +
 | 
|  class base_job(object):
 | 
|      """An abstract base class for the various autotest job classes.
 | 
|  
 | 
| @@ -799,6 +996,11 @@ class base_job(object):
 | 
|          # initialize all the job state
 | 
|          self._state = self._job_state()
 | 
|  
 | 
| +        # initialize tap reporting
 | 
| +        if dargs.has_key('options'):
 | 
| +            self._tap = self._tap_init(dargs['options'].tap_report)
 | 
| +        else:
 | 
| +            self._tap = self._tap_init(False)
 | 
|  
 | 
|      @classmethod
 | 
|      def _find_base_directories(cls):
 | 
| @@ -959,6 +1161,11 @@ class base_job(object):
 | 
|                                subdir, e)
 | 
|              raise error.TestError('%s directory creation failed' % subdir)
 | 
|  
 | 
| +    def _tap_init(self, enable):
 | 
| +        """Initialize TAP reporting
 | 
| +        """
 | 
| +        return TAPReport(enable, resultdir=self.resultdir)
 | 
| +
 | 
|  
 | 
|      def record(self, status_code, subdir, operation, status='',
 | 
|                 optional_fields=None):
 | 
| 
 |