OLD | NEW |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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 Loading... |
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) |
OLD | NEW |