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