Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(570)

Side by Side Diff: client/common_lib/base_job.py

Issue 6246035: Merge remote branch 'cros/upstream' into master (Closed) Base URL: ssh://git@gitrw.chromium.org:9222/autotest.git@master
Patch Set: patch Created 9 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 import os, copy, logging, errno, fcntl, time, re, weakref, traceback 1 import os, copy, logging, errno, fcntl, time, re, weakref, traceback
2 import tarfile
2 import cPickle as pickle 3 import cPickle as pickle
3
4 from autotest_lib.client.common_lib import autotemp, error, log 4 from autotest_lib.client.common_lib import autotemp, error, log
5 5
6 6
7 class job_directory(object): 7 class job_directory(object):
8 """Represents a job.*dir directory.""" 8 """Represents a job.*dir directory."""
9 9
10 10
11 class JobDirectoryException(error.AutotestError): 11 class JobDirectoryException(error.AutotestError):
12 """Generic job_directory exception superclass.""" 12 """Generic job_directory exception superclass."""
13 13
(...skipping 401 matching lines...) Expand 10 before | Expand all | Expand 10 after
415 return property(getter, setter) 415 return property(getter, setter)
416 416
417 417
418 class status_log_entry(object): 418 class status_log_entry(object):
419 """Represents a single status log entry.""" 419 """Represents a single status log entry."""
420 420
421 RENDERED_NONE_VALUE = '----' 421 RENDERED_NONE_VALUE = '----'
422 TIMESTAMP_FIELD = 'timestamp' 422 TIMESTAMP_FIELD = 'timestamp'
423 LOCALTIME_FIELD = 'localtime' 423 LOCALTIME_FIELD = 'localtime'
424 424
425 # non-space whitespace is forbidden in any fields
426 BAD_CHAR_REGEX = re.compile(r'[\t\n\r\v\f]')
427
425 def __init__(self, status_code, subdir, operation, message, fields, 428 def __init__(self, status_code, subdir, operation, message, fields,
426 timestamp=None): 429 timestamp=None):
427 """Construct a status.log entry. 430 """Construct a status.log entry.
428 431
429 @param status_code: A message status code. Must match the codes 432 @param status_code: A message status code. Must match the codes
430 accepted by autotest_lib.common_lib.log.is_valid_status. 433 accepted by autotest_lib.common_lib.log.is_valid_status.
431 @param subdir: A valid job subdirectory, or None. 434 @param subdir: A valid job subdirectory, or None.
432 @param operation: Description of the operation, or None. 435 @param operation: Description of the operation, or None.
433 @param message: A printable string describing event to be recorded. 436 @param message: A printable string describing event to be recorded.
434 @param fields: A dictionary of arbitrary alphanumeric key=value pairs 437 @param fields: A dictionary of arbitrary alphanumeric key=value pairs
435 to be included in the log, or None. 438 to be included in the log, or None.
436 @param timestamp: An optional integer timestamp, in the same format 439 @param timestamp: An optional integer timestamp, in the same format
437 as a time.time() timestamp. If unspecified, the current time is 440 as a time.time() timestamp. If unspecified, the current time is
438 used. 441 used.
439 442
440 @raise ValueError: if any of the parameters are invalid 443 @raise ValueError: if any of the parameters are invalid
441 """ 444 """
442 # non-space whitespace is forbidden in any fields
443 bad_char_regex = r'[\t\n\r\v\f]'
444 445
445 if not log.is_valid_status(status_code): 446 if not log.is_valid_status(status_code):
446 raise ValueError('status code %r is not valid' % status_code) 447 raise ValueError('status code %r is not valid' % status_code)
447 self.status_code = status_code 448 self.status_code = status_code
448 449
449 if subdir and re.search(bad_char_regex, subdir): 450 if subdir and self.BAD_CHAR_REGEX.search(subdir):
450 raise ValueError('Invalid character in subdir string') 451 raise ValueError('Invalid character in subdir string')
451 self.subdir = subdir 452 self.subdir = subdir
452 453
453 if operation and re.search(bad_char_regex, operation): 454 if operation and self.BAD_CHAR_REGEX.search(operation):
454 raise ValueError('Invalid character in operation string') 455 raise ValueError('Invalid character in operation string')
455 self.operation = operation 456 self.operation = operation
456 457
457 # break the message line into a single-line message that goes into the 458 # break the message line into a single-line message that goes into the
458 # database, and a block of additional lines that goes into the status 459 # database, and a block of additional lines that goes into the status
459 # log but will never be parsed 460 # log but will never be parsed
460 message_lines = message.split('\n') 461 message_lines = message.split('\n')
461 self.message = message_lines[0].replace('\t', ' ' * 8) 462 self.message = message_lines[0].replace('\t', ' ' * 8)
462 self.extra_message_lines = message_lines[1:] 463 self.extra_message_lines = message_lines[1:]
463 if re.search(bad_char_regex, self.message): 464 if self.BAD_CHAR_REGEX.search(self.message):
464 raise ValueError('Invalid character in message %r' % self.message) 465 raise ValueError('Invalid character in message %r' % self.message)
465 466
466 if not fields: 467 if not fields:
467 self.fields = {} 468 self.fields = {}
468 else: 469 else:
469 self.fields = fields.copy() 470 self.fields = fields.copy()
470 for key, value in self.fields.iteritems(): 471 for key, value in self.fields.iteritems():
471 if re.search(bad_char_regex, key + value): 472 if self.BAD_CHAR_REGEX.search(key + value):
472 raise ValueError('Invalid character in %r=%r field' 473 raise ValueError('Invalid character in %r=%r field'
473 % (key, value)) 474 % (key, value))
474 475
475 # build up the timestamp 476 # build up the timestamp
476 if timestamp is None: 477 if timestamp is None:
477 timestamp = int(time.time()) 478 timestamp = int(time.time())
478 self.fields[self.TIMESTAMP_FIELD] = str(timestamp) 479 self.fields[self.TIMESTAMP_FIELD] = str(timestamp)
479 self.fields[self.LOCALTIME_FIELD] = time.strftime( 480 self.fields[self.LOCALTIME_FIELD] = time.strftime(
480 '%b %d %H:%M:%S', time.localtime(timestamp)) 481 '%b %d %H:%M:%S', time.localtime(timestamp))
481 482
(...skipping 85 matching lines...) Expand 10 before | Expand all | Expand 10 after
567 568
568 569
569 class status_logger(object): 570 class status_logger(object):
570 """Represents a status log file. Responsible for translating messages 571 """Represents a status log file. Responsible for translating messages
571 into on-disk status log lines. 572 into on-disk status log lines.
572 573
573 @property global_filename: The filename to write top-level logs to. 574 @property global_filename: The filename to write top-level logs to.
574 @property subdir_filename: The filename to write subdir-level logs to. 575 @property subdir_filename: The filename to write subdir-level logs to.
575 """ 576 """
576 def __init__(self, job, indenter, global_filename='status', 577 def __init__(self, job, indenter, global_filename='status',
577 subdir_filename='status', record_hook=None): 578 subdir_filename='status', record_hook=None,
579 tap_writer=None):
578 """Construct a logger instance. 580 """Construct a logger instance.
579 581
580 @param job: A reference to the job object this is logging for. Only a 582 @param job: A reference to the job object this is logging for. Only a
581 weak reference to the job is held, to avoid a 583 weak reference to the job is held, to avoid a
582 status_logger <-> job circular reference. 584 status_logger <-> job circular reference.
583 @param indenter: A status_indenter instance, for tracking the 585 @param indenter: A status_indenter instance, for tracking the
584 indentation level. 586 indentation level.
585 @param global_filename: An optional filename to initialize the 587 @param global_filename: An optional filename to initialize the
586 self.global_filename attribute. 588 self.global_filename attribute.
587 @param subdir_filename: An optional filename to initialize the 589 @param subdir_filename: An optional filename to initialize the
588 self.subdir_filename attribute. 590 self.subdir_filename attribute.
589 @param record_hook: An optional function to be called before an entry 591 @param record_hook: An optional function to be called before an entry
590 is logged. The function should expect a single parameter, a 592 is logged. The function should expect a single parameter, a
591 copy of the status_log_entry object. 593 copy of the status_log_entry object.
594 @param tap_writer: An instance of the class TAPReport for addionally
595 writing TAP files
592 """ 596 """
593 self._jobref = weakref.ref(job) 597 self._jobref = weakref.ref(job)
594 self._indenter = indenter 598 self._indenter = indenter
595 self.global_filename = global_filename 599 self.global_filename = global_filename
596 self.subdir_filename = subdir_filename 600 self.subdir_filename = subdir_filename
597 self._record_hook = record_hook 601 self._record_hook = record_hook
602 if tap_writer is None:
603 self._tap_writer = TAPReport(None)
604 else:
605 self._tap_writer = tap_writer
598 606
599 607
600 def render_entry(self, log_entry): 608 def render_entry(self, log_entry):
601 """Render a status_log_entry as it would be written to a log file. 609 """Render a status_log_entry as it would be written to a log file.
602 610
603 @param log_entry: A status_log_entry instance to be rendered. 611 @param log_entry: A status_log_entry instance to be rendered.
604 612
605 @return: The status log entry, rendered as it would be written to the 613 @return: The status log entry, rendered as it would be written to the
606 logs (including indentation). 614 logs (including indentation).
607 """ 615 """
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after
640 648
641 # write out to entry to the log files 649 # write out to entry to the log files
642 log_text = self.render_entry(log_entry) 650 log_text = self.render_entry(log_entry)
643 for log_file in log_files: 651 for log_file in log_files:
644 fileobj = open(log_file, 'a') 652 fileobj = open(log_file, 'a')
645 try: 653 try:
646 print >> fileobj, log_text 654 print >> fileobj, log_text
647 finally: 655 finally:
648 fileobj.close() 656 fileobj.close()
649 657
658 # write to TAPRecord instance
659 if log_entry.is_end() and self._tap_writer.do_tap_report:
660 self._tap_writer.record(log_entry, self._indenter.indent, log_files)
661
650 # adjust the indentation if this was a START or END entry 662 # adjust the indentation if this was a START or END entry
651 if log_entry.is_start(): 663 if log_entry.is_start():
652 self._indenter.increment() 664 self._indenter.increment()
653 elif log_entry.is_end(): 665 elif log_entry.is_end():
654 self._indenter.decrement() 666 self._indenter.decrement()
655 667
656 668
669 class TAPReport(object):
670 """
671 Deal with TAP reporting for the Autotest client.
672 """
673
674 job_statuses = {
675 "TEST_NA": False,
676 "ABORT": False,
677 "ERROR": False,
678 "FAIL": False,
679 "WARN": False,
680 "GOOD": True,
681 "START": True,
682 "END GOOD": True,
683 "ALERT": False,
684 "RUNNING": False,
685 "NOSTATUS": False
686 }
687
688
689 def __init__(self, enable, resultdir=None, global_filename='status'):
690 """
691 @param enable: Set self.do_tap_report to trigger TAP reporting.
692 @param resultdir: Path where the TAP report files will be written.
693 @param global_filename: File name of the status files .tap extensions
694 will be appended.
695 """
696 self.do_tap_report = enable
697 if resultdir is not None:
698 self.resultdir = os.path.abspath(resultdir)
699 self._reports_container = {}
700 self._keyval_container = {} # {'path1': [entries],}
701 self.global_filename = global_filename
702
703
704 @classmethod
705 def tap_ok(self, success, counter, message):
706 """
707 return a TAP message string.
708
709 @param success: True for positive message string.
710 @param counter: number of TAP line in plan.
711 @param message: additional message to report in TAP line.
712 """
713 if success:
714 message = "ok %s - %s" % (counter, message)
715 else:
716 message = "not ok %s - %s" % (counter, message)
717 return message
718
719
720 def record(self, log_entry, indent, log_files):
721 """
722 Append a job-level status event to self._reports_container. All
723 events will be written to TAP log files at the end of the test run.
724 Otherwise, it's impossilble to determine the TAP plan.
725
726 @param log_entry: A string status code describing the type of status
727 entry being recorded. It must pass log.is_valid_status to be
728 considered valid.
729 @param indent: Level of the log_entry to determine the operation if
730 log_entry.operation is not given.
731 @param log_files: List of full path of files the TAP report will be
732 written to at the end of the test.
733 """
734 for log_file in log_files:
735 log_file_path = os.path.dirname(log_file)
736 key = log_file_path.split(self.resultdir, 1)[1].strip(os.sep)
737 if not key:
738 key = 'root'
739
740 if not self._reports_container.has_key(key):
741 self._reports_container[key] = []
742
743 if log_entry.operation:
744 operation = log_entry.operation
745 elif indent == 1:
746 operation = "job"
747 else:
748 operation = "unknown"
749 entry = self.tap_ok(
750 self.job_statuses.get(log_entry.status_code, False),
751 len(self._reports_container[key]) + 1, operation + "\n"
752 )
753 self._reports_container[key].append(entry)
754
755
756 def record_keyval(self, path, dictionary, type_tag=None):
757 """
758 Append a key-value pairs of dictionary to self._keyval_container in
759 TAP format. Once finished write out the keyval.tap file to the file
760 system.
761
762 If type_tag is None, then the key must be composed of alphanumeric
763 characters (or dashes + underscores). However, if type-tag is not
764 null then the keys must also have "{type_tag}" as a suffix. At
765 the moment the only valid values of type_tag are "attr" and "perf".
766
767 @param path: The full path of the keyval.tap file to be created
768 @param dictionary: The keys and values.
769 @param type_tag: The type of the values
770 """
771 self._keyval_container.setdefault(path, [0, []])
772 self._keyval_container[path][0] += 1
773
774 if type_tag is None:
775 key_regex = re.compile(r'^[-\.\w]+$')
776 else:
777 if type_tag not in ('attr', 'perf'):
778 raise ValueError('Invalid type tag: %s' % type_tag)
779 escaped_tag = re.escape(type_tag)
780 key_regex = re.compile(r'^[-\.\w]+\{%s\}$' % escaped_tag)
781 self._keyval_container[path][1].extend([
782 self.tap_ok(True, self._keyval_container[path][0], "results"),
783 "\n ---\n",
784 ])
785 try:
786 for key in sorted(dictionary.keys()):
787 if not key_regex.search(key):
788 raise ValueError('Invalid key: %s' % key)
789 self._keyval_container[path][1].append(
790 ' %s: %s\n' % (key.replace('{', '_').rstrip('}'),
791 dictionary[key])
792 )
793 finally:
794 self._keyval_container[path][1].append(" ...\n")
795 self._write_keyval()
796
797
798 def _write_reports(self):
799 """
800 Write TAP reports to file.
801 """
802 for key in self._reports_container.keys():
803 if key == 'root':
804 sub_dir = ''
805 else:
806 sub_dir = key
807 tap_fh = open(os.sep.join(
808 [self.resultdir, sub_dir, self.global_filename]
809 ) + ".tap", 'w')
810 tap_fh.write('1..' + str(len(self._reports_container[key])) + '\n')
811 tap_fh.writelines(self._reports_container[key])
812 tap_fh.close()
813
814
815 def _write_keyval(self):
816 """
817 Write the self._keyval_container key values to a file.
818 """
819 for path in self._keyval_container.keys():
820 tap_fh = open(path + ".tap", 'w')
821 tap_fh.write('1..' + str(self._keyval_container[path][0]) + '\n')
822 tap_fh.writelines(self._keyval_container[path][1])
823 tap_fh.close()
824
825
826 def write(self):
827 """
828 Write the TAP reports to files.
829 """
830 self._write_reports()
831
832
833 def _write_tap_archive(self):
834 """
835 Write a tar archive containing all the TAP files and
836 a meta.yml containing the file names.
837 """
838 os.chdir(self.resultdir)
839 tap_files = []
840 for rel_path, d, files in os.walk('.'):
841 tap_files.extend(["/".join(
842 [rel_path, f]) for f in files if f.endswith('.tap')])
843 meta_yaml = open('meta.yml', 'w')
844 meta_yaml.write('file_order:\n')
845 tap_tar = tarfile.open(self.resultdir + '/tap.tar.gz', 'w:gz')
846 for f in tap_files:
847 meta_yaml.write(" - " + f.lstrip('./') + "\n")
848 tap_tar.add(f)
849 meta_yaml.close()
850 tap_tar.add('meta.yml')
851 tap_tar.close()
852
853
657 class base_job(object): 854 class base_job(object):
658 """An abstract base class for the various autotest job classes. 855 """An abstract base class for the various autotest job classes.
659 856
660 @property autodir: The top level autotest directory. 857 @property autodir: The top level autotest directory.
661 @property clientdir: The autotest client directory. 858 @property clientdir: The autotest client directory.
662 @property serverdir: The autotest server directory. [OPTIONAL] 859 @property serverdir: The autotest server directory. [OPTIONAL]
663 @property resultdir: The directory where results should be written out. 860 @property resultdir: The directory where results should be written out.
664 [WRITABLE] 861 [WRITABLE]
665 862
666 @property pkgdir: The job packages directory. [WRITABLE] 863 @property pkgdir: The job packages directory. [WRITABLE]
(...skipping 125 matching lines...) Expand 10 before | Expand all | Expand 10 after
792 989
793 # initialize all the other directories relative to the base ones 990 # initialize all the other directories relative to the base ones
794 self._initialize_dir_properties() 991 self._initialize_dir_properties()
795 self._resultdir = self._job_directory( 992 self._resultdir = self._job_directory(
796 self._find_resultdir(*args, **dargs), True) 993 self._find_resultdir(*args, **dargs), True)
797 self._execution_contexts = [] 994 self._execution_contexts = []
798 995
799 # initialize all the job state 996 # initialize all the job state
800 self._state = self._job_state() 997 self._state = self._job_state()
801 998
999 # initialize tap reporting
1000 if dargs.has_key('options'):
1001 self._tap = self._tap_init(dargs['options'].tap_report)
1002 else:
1003 self._tap = self._tap_init(False)
802 1004
803 @classmethod 1005 @classmethod
804 def _find_base_directories(cls): 1006 def _find_base_directories(cls):
805 raise NotImplementedError() 1007 raise NotImplementedError()
806 1008
807 1009
808 def _initialize_dir_properties(self): 1010 def _initialize_dir_properties(self):
809 """ 1011 """
810 Initializes all the secondary self.*dir properties. Requires autodir, 1012 Initializes all the secondary self.*dir properties. Requires autodir,
811 clientdir and serverdir to already be initialized. 1013 clientdir and serverdir to already be initialized.
(...skipping 140 matching lines...) Expand 10 before | Expand all | Expand 10 after
952 1154
953 # create the outputdir and raise a TestError if it isn't valid 1155 # create the outputdir and raise a TestError if it isn't valid
954 try: 1156 try:
955 outputdir = self._job_directory(path, True) 1157 outputdir = self._job_directory(path, True)
956 return outputdir 1158 return outputdir
957 except self._job_directory.JobDirectoryException, e: 1159 except self._job_directory.JobDirectoryException, e:
958 logging.exception('%s directory creation failed with %s', 1160 logging.exception('%s directory creation failed with %s',
959 subdir, e) 1161 subdir, e)
960 raise error.TestError('%s directory creation failed' % subdir) 1162 raise error.TestError('%s directory creation failed' % subdir)
961 1163
1164 def _tap_init(self, enable):
1165 """Initialize TAP reporting
1166 """
1167 return TAPReport(enable, resultdir=self.resultdir)
1168
962 1169
963 def record(self, status_code, subdir, operation, status='', 1170 def record(self, status_code, subdir, operation, status='',
964 optional_fields=None): 1171 optional_fields=None):
965 """Record a job-level status event. 1172 """Record a job-level status event.
966 1173
967 Logs an event noteworthy to the Autotest job as a whole. Messages will 1174 Logs an event noteworthy to the Autotest job as a whole. Messages will
968 be written into a global status log file, as well as a subdir-local 1175 be written into a global status log file, as well as a subdir-local
969 status log file (if subdir is specified). 1176 status log file (if subdir is specified).
970 1177
971 @param status_code: A string status code describing the type of status 1178 @param status_code: A string status code describing the type of status
(...skipping 18 matching lines...) Expand all
990 """Record a job-level status event, using a status_log_entry. 1197 """Record a job-level status event, using a status_log_entry.
991 1198
992 This is the same as self.record but using an existing status log 1199 This is the same as self.record but using an existing status log
993 entry object rather than constructing one for you. 1200 entry object rather than constructing one for you.
994 1201
995 @param entry: A status_log_entry object 1202 @param entry: A status_log_entry object
996 @param log_in_subdir: A boolean that indicates (when true) that subdir 1203 @param log_in_subdir: A boolean that indicates (when true) that subdir
997 logs should be written into the subdirectory status log file. 1204 logs should be written into the subdirectory status log file.
998 """ 1205 """
999 self._get_status_logger().record_entry(entry, log_in_subdir) 1206 self._get_status_logger().record_entry(entry, log_in_subdir)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698