#!/usr/bin/python # Copyright (c) 2012 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. """Report summarizer of internal test pass% from running many tests in LTP. LTP is the Linux Test Project from http://ltp.sourceforge.net/. This script serves to summarize the results of a test run by LTP test infrastructure. LTP frequently runs >1000 tests so summarizing the results by result-type and count is useful. This script is invoked by the ltp.py wrapper in Autotest as a post-processing step to summarize the LTP run results in the Autotest log file. This script may be invoked by the command-line as follows: $ ./parse_ltp_out.py -l /mypath/ltp.out """ import optparse import os import re import sys SUMMARY_BORDER = 80 * '-' # Prefix char used in summaries: # +: sums into 'passing' # -: sums into 'notpassing' TEST_FILTERS = {'TPASS': '+Pass', 'TFAIL': '-Fail', 'TBROK': '-Broken', 'TCONF': '-Config error', 'TRETR': 'Retired', 'TWARN': '+Warning'} def parse_args(argv): """Setup command line parsing options. @param argv: command-line arguments. @return parsed option result from optparse. """ parser = optparse.OptionParser('Usage: %prog --ltp-out-file=/path/ltp.out') parser.add_option( '-l', '--ltp-out-file', help='[required] Path and file name for ltp.out [default: %default]', dest='ltp_out_file', default=None) parser.add_option( '-t', '--timings', help='Show test timings in buckets [default: %default]', dest='test_timings', action='store_true', default=False) options, args = parser.parse_args() if not options.ltp_out_file: parser.error('You must supply a value for --ltp-out-file.') return options def _filter_and_count(ltp_out_file, test_filters): """Utility function to count lines that match certain filters. @param ltp_out_file: human-readable output file from LTP -p (ltp.out). @param test_filters: dict of the tags to match and corresponding print tags. @return a dictionary with counts of the lines that matched each tag. """ marker_line = '^<<<%s>>>$' status_line_re = re.compile('^\w+ +\d+ +(\w+) +: +\w+') filter_accumulator = dict.fromkeys(test_filters.keys(), 0) parse_states = ( {'filters': {}, 'terminator': re.compile(marker_line % 'test_output')}, {'filters': filter_accumulator, 'terminator': re.compile(marker_line % 'execution_status')}) # Simple 2-state state machine. state_test_active = False with open(ltp_out_file) as f: for line in f: state_index = int(state_test_active) if re.match(parse_states[state_index]['terminator'], line): # This state is terminated - proceed to next. state_test_active = not state_test_active else: # Determine if this line matches any of the sought tags. m = re.match(status_line_re, line) if m and m.group(1) in parse_states[state_index]['filters']: parse_states[state_index]['filters'][m.group(1)] += 1 return filter_accumulator def _print_summary(filters, accumulator): """Utility function to print the summary of the parsing of ltp.out. Prints a count of each type of test result, then a %pass-rate score. @param filters: map of tags sought and corresponding print headers. @param accumulator: counts of test results with same keys as filters. """ print SUMMARY_BORDER print 'Linux Test Project (LTP) Run Summary:' print SUMMARY_BORDER # Size the header to the largest printable tag. fmt = '%%%ss: %%s' % max(map(lambda x: len(x), filters.values())) for k in sorted(filters): print fmt % (filters[k], accumulator[k]) print SUMMARY_BORDER # These calculations from ltprun-summary.sh script. pass_count = sum([accumulator[k] for k in filters if filters[k][0] == '+']) notpass_count = sum([accumulator[k] for k in filters if filters[k][0] == '-']) total_count = pass_count + notpass_count if total_count: score = float(pass_count) / float(total_count) * 100.0 else: score = 0.0 print 'SCORE.ltp: %.2f' % score print SUMMARY_BORDER def _filter_times(ltp_out_file): """Utility function to count lines that match certain filters. @param ltp_out_file: human-readable output file from LTP -p (ltp.out). @return a dictionary with test tags and corresponding times. The dictionary is a set of buckets of tests based on the test duration: 0: [tests that recoreded 0sec runtimes], 1: [tests that recorded runtimes from 0-60sec], ... 2: [tests that recorded runtimes from 61-120sec], ... """ test_tag_line_re = re.compile('^tag=(\w+)\s+stime=(\d+)$') test_duration_line_re = re.compile('^duration=(\d+)\s+.*') filter_accumulator = {} with open(ltp_out_file) as f: previous_tag = None previous_time_s = 0 recorded_tags = set() for line in f: tag_matches = re.match(test_tag_line_re, line) if tag_matches: current_tag = tag_matches.group(1) if previous_tag: if previous_tag in recorded_tags: print 'WARNING: duplicate tag found: %s.' % previous_tag previous_tag = current_tag continue duration_matches = re.match(test_duration_line_re, line) if duration_matches: duration = int(duration_matches.group(1)) if not previous_tag: print 'WARNING: duration without a tag: %s.' % duration continue if duration != 0: duration = int(duration / 60) + 1 test_list = filter_accumulator.setdefault(duration, []) test_list.append(previous_tag) return filter_accumulator def _print_timings(accumulator): """Utility function to print the summary of the parsing of ltp.out. Prints a count of each type of test result, then a %pass-rate score. Args: @param accumulator: counts of test results """ print SUMMARY_BORDER print 'Linux Test Project (LTP) Timing Summary:' print SUMMARY_BORDER for test_limit in sorted(accumulator.keys()): print '<=%smin: %s tags: %s' % ( test_limit, len(accumulator[test_limit]), ', '.join(sorted(accumulator[test_limit]))) print '' print SUMMARY_BORDER return def summarize(ltp_out_file, test_timings=None): """Scan detailed output from LTP run for summary test status reporting. Looks for all possible test result types know to LTP: pass, fail, broken, config error, retired and warning. Prints a summary. @param ltp_out_file: human-readable output file from LTP -p (ltp.out). @param test_timings: if True, emit an ordered summary of run timings of tests. """ if not os.path.isfile(ltp_out_file): print 'Unable to locate %s.' % ltp_out_file return _print_summary(TEST_FILTERS, _filter_and_count(ltp_out_file, TEST_FILTERS)) if test_timings: _print_timings(_filter_times(ltp_out_file)) def main(argv): """ Parse the human-readable logs from an LTP run and print a summary. @param argv: command-line arguments. """ options = parse_args(argv) summarize(options.ltp_out_file, options.test_timings) if __name__ == '__main__': main(sys.argv)