Home | History | Annotate | Download | only in ltp
      1 #!/usr/bin/env python3
      2 """
      3     An LTP [execution and] parsing wrapper.
      4 
      5     Used as a second layer for ease-of-use with users as many developers
      6     complain about complexity involved with trying to use LTP in my
      7     organization -_-.
      8 
      9     Copyright (C) 2009-2012, Ngie Cooper
     10 
     11     This program is free software; you can redistribute it and/or modify
     12     it under the terms of the GNU General Public License as published by
     13     the Free Software Foundation; either version 2 of the License, or
     14     (at your option) any later version.
     15 
     16     This program is distributed in the hope that it will be useful,
     17     but WITHOUT ANY WARRANTY; without even the implied warranty of
     18     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     19     GNU General Public License for more details.
     20 
     21     You should have received a copy of the GNU General Public License along
     22     with this program; if not, write to the Free Software Foundation, Inc.,
     23     51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
     24 """
     25 
     26 
     27 from optparse import OptionGroup, OptionParser
     28 import os
     29 import re
     30 import sys
     31 
     32 
     33 class ResultsParseException(Exception):
     34     """ Extended class for parsing LTP results. """
     35 
     36 
     37 def parse_ltp_results(exec_log, output_log, verbose=0):
     38     """Function for parsing LTP results.
     39 
     40     1. The exec log is the log with the results in summary form.
     41 
     42     And now a note from our sponsors about exec logs...
     43 
     44     startup='Thu Oct  1 06:42:07 2009'
     45     tag=abort01 stime=1254379327 dur=2 exit=exited stat=0 core=no cu=0 cs=16
     46     tag=accept01 stime=1254379329 dur=0 exit=exited stat=0 core=no cu=1 cs=0
     47     tag=access01 stime=1254379329 dur=0 exit=exited stat=0 core=no cu=0 cs=0
     48     tag=access02 stime=1254379329 dur=0 exit=exited stat=0 core=no cu=0 cs=0
     49     tag=access03 stime=1254379329 dur=1 exit=exited stat=0 core=no cu=0 cs=1
     50 
     51     [...]
     52 
     53     a. tag is the test tag name.
     54     b. stime is the system time at the start of the exec.
     55     c. dur is the total duration of the test.
     56     d. exit tells you what the result was. Valid values are:
     57        - exited
     58        - signaled
     59        - stopped
     60        - unknown
     61        See run_child in pan.c.
     62     e. stat is the exit status.
     63     f. core answers the question: `did I dump core?'.
     64     g. cu is the cutime (cumulative user time).
     65     h. cs is the cstime (cumulative system time).
     66 
     67     2. The output log is the log with all of the terse results.
     68     3. verbose tells us whether or not we need to include the passed results.
     69     """
     70 
     71     if not os.access(exec_log, os.R_OK):
     72         raise ResultsParseException("Exec log - %s - specified doesn't exist"
     73                                     % exec_log)
     74     elif 1 < verbose and not os.access(output_log, os.R_OK):
     75         # Need the output log for context to the end user.
     76         raise ResultsParseException("Output log - %s - specified doesn't exist"
     77                                     % output_log)
     78 
     79     context = None
     80 
     81     failed = []
     82     passed = 0
     83 
     84     if 2 <= verbose:
     85         passed = []
     86 
     87     target_vals = ('exited', '0', 'no')
     88 
     89     fd = open(exec_log, 'r')
     90 
     91     try:
     92         content = fd.read()
     93         matches = re.finditer('tag=(?P<tag>\w+).+exit=(?P<exit>\w+) '
     94                               'stat=(?P<stat>\d+) core=(?P<core>\w+)', content)
     95     finally:
     96         content = None
     97         fd.close()
     98 
     99     if not matches:
    100         raise ResultsParseException("No parseable results were found in the "
    101                                     "exec log - `%s'." % exec_log)
    102 
    103     for match in matches:
    104 
    105         if ((match.group('exit'), match.group('stat'), match.group('core')) !=
    106              target_vals):
    107             failed.append(match.group('tag'))
    108         elif 2 <= verbose:
    109             passed.append(match.group('tag'))
    110         else:
    111             passed += 1
    112 
    113     # Save memory on large files because lists can eat up a fair amount of
    114     # memory.
    115     matches = None
    116 
    117     if 1 <= verbose:
    118 
    119         context = {}
    120 
    121         search_tags = failed[:]
    122 
    123         if 2 <= verbose:
    124             search_tags += passed
    125 
    126         search_tags.sort()
    127 
    128         fd = open(output_log, 'r')
    129 
    130         try:
    131 
    132             line_iterator = getattr(fd, 'xreadlines', getattr(fd, 'readlines'))
    133 
    134             end_output = '<<<execution_status>>>'
    135             output_start = '<<<test_output>>>'
    136 
    137             tag_re = re.compile('tag=(\w+)')
    138 
    139             grab_output = False
    140 
    141             local_context = ''
    142 
    143             search_tag = None
    144 
    145             try:
    146 
    147                 while True:
    148 
    149                     line = next(line_iterator)
    150 
    151                     if line.startswith(end_output):
    152 
    153                         if search_tag:
    154                             context[search_tag] = local_context
    155 
    156                         grab_output = False
    157                         local_context = ''
    158                         search_tag = None
    159 
    160                     if not search_tag:
    161 
    162                         while True:
    163 
    164                             line = next(line_iterator)
    165 
    166                             match = tag_re.match(line)
    167 
    168                             if match and match.group(1) in search_tags:
    169                                 search_tag = match.group(1)
    170                                 break
    171 
    172                     elif line.startswith(output_start):
    173                         grab_output = True
    174                     elif grab_output:
    175                         local_context += line
    176 
    177             except StopIteration:
    178                 pass
    179 
    180             for k in list(context.keys()):
    181                 if k not in search_tags:
    182                     raise ResultsParseException('Leftover token in search '
    183                                                 'keys: %s' % k)
    184 
    185         except Exception as exc:
    186             # XXX (garrcoop): change from Exception to soft error and print
    187             # out warning with logging module.
    188             raise ResultsParseException('Encountered exception reading output '
    189                                         'for context: %s' % str(exc))
    190         finally:
    191             fd.close()
    192 
    193     return failed, passed, context
    194 
    195 
    196 def determine_context(output_log, testsuite, test_set, context):
    197     """Return a set of context values mapping test_set -> context."""
    198 
    199     test_set_context = {}
    200 
    201     for test in test_set:
    202 
    203         if test in context:
    204             test_context = context[test]
    205             del context[test]
    206         else:
    207             test_context = ('Could not determine context for %s; please see '
    208                             'output log - %s' % (test, output_log))
    209 
    210         test_set_context['%s : %s' % (testsuite, test)] = test_context
    211 
    212     return test_set_context
    213 
    214 
    215 def print_context(output_dest, header, testsuite_context):
    216     """Print out testsuite_context to output_dest, heading it up with
    217        header.
    218     """
    219 
    220     output_dest.write('\n'.join(['', '=' * 40, header, '-' * 40, '']))
    221 
    222     for test, context in list(testsuite_context.items()):
    223         output_dest.write('<output test="%s">\n%s\n</output>\n' %
    224                           (test, context.strip()))
    225 
    226 
    227 def main():
    228     """main"""
    229 
    230     parser = OptionParser(prog=os.path.basename(sys.argv[0]),
    231                           usage='usage: %prog [options] test ...',
    232                           version='0.0.2')
    233 
    234     ltpdir = os.getenv('LTPROOT', '@prefix@')
    235 
    236     parser.add_option('-l', '--ltp-dir', dest='ltp_dir',
    237                       default=ltpdir, help='LTP directory [default: %default]')
    238     parser.add_option('-L', '--log-dir', dest='log_dir',
    239                       default=None,
    240                       help=('directory for [storing and] retrieving logs '
    241                             '[default: %s/output]' % ltpdir),
    242                       metavar='DIR')
    243     parser.add_option('-p', '--postprocess-only', dest='postprocess_only',
    244                       default=False, action='store_true',
    245                       help=("Don't execute runltp; just postprocess logs "
    246                             "[default: %default]."))
    247     parser.add_option('-o', '--output-file', dest='output_file',
    248                       default=None,
    249                       help='File to output results')
    250     parser.add_option('-r', '--runltp-opts', dest='runltp_opts',
    251                       default='',
    252                       help=('options to pass directly to runltp (will '
    253                             'suppress -q).'))
    254 
    255     group = OptionGroup(parser, 'Logging',
    256                         'If --summary-mode is 0, then the summary output is '
    257                         'suppressed. '
    258                         'If --summary-mode is 1 [the default], then summary '
    259                         'output will be displayed for test execution'
    260                         'If --summary-mode is 2, then summary output will be '
    261                         'provided on a per-test suite basis. If only '
    262                         'one test suite is specified, this has the same net '
    263                         "effect as `--summary-mode 1'"
    264                         'If --verbose is specified once, prints out failed '
    265                         'test information with additional context. '
    266                         'If --verbose is specified twice, prints out the '
    267                         'failed and passed test context, as well as the '
    268                         'summary.')
    269 
    270     parser.add_option('-s', '--summary-mode', dest='summary_mode', default=1,
    271                       type='int',
    272                       help='See Logging.')
    273 
    274     parser.add_option('-v', '--verbose', dest='verbose', default=0,
    275                       action='count',
    276                       help=('Increases context verbosity from tests. See '
    277                             'Verbosity for more details.'))
    278     parser.add_option_group(group)
    279 
    280     group = OptionGroup(parser, 'Copyright',
    281                         '%(prog)s version %(version)s, Copyright (C) '
    282                         '2009-2012, Ngie Cooper %(prog)s comes with '
    283                         'ABSOLUTELY NO WARRANTY; '
    284                         'This is free software, and you are welcome to '
    285                         'redistribute it under certain conditions (See the '
    286                         'license tort in %(file)s for more details).'
    287                         % { 'file'    : os.path.abspath(__file__),
    288                             'prog'    : parser.prog,
    289                             'version' : parser.version })
    290 
    291     parser.add_option_group(group)
    292 
    293     opts, args = parser.parse_args()
    294 
    295     # Remove -q from the opts string, as long as it's a standalone option.
    296     runltp_opts = re.sub('^((?<!\S)+\-q\s+|\-q|\s+\-q(?!\S))$', '',
    297                          opts.runltp_opts)
    298 
    299     if not opts.log_dir:
    300         opts.log_dir = os.path.join(opts.ltp_dir, 'output')
    301 
    302     if not opts.summary_mode and not opts.verbose:
    303         parser.error('You cannot suppress summary output and disable '
    304                      'verbosity.')
    305     elif opts.summary_mode not in list(range(3)):
    306         parser.error('--summary-mode must be a value between 0 and 2.')
    307 
    308     if len(args) == 0:
    309         # Default to scenarios also used by runltp.
    310         fd = open(os.path.join(ltpdir, 'scenario_groups/default'), 'r')
    311         try:
    312             args = [l.strip() for l in fd.readlines()]
    313         finally:
    314             fd.close()
    315 
    316     if opts.output_file:
    317 
    318         output_dir = os.path.dirname(opts.output_file)
    319 
    320         if output_dir:
    321             # Not cwd; let's check to make sure that the directory does or
    322             # does not exist.
    323 
    324             if not os.path.exists(output_dir):
    325                 # We need to make the directory.
    326                 os.makedirs(os.path.dirname(opts.output_file))
    327             elif not os.path.isdir(os.path.abspath(output_dir)):
    328                 # Path exists, but isn't a file. Oops!
    329                 parser.error('Dirname for path specified - %s - is not valid'
    330                              % output_dir)
    331 
    332         else:
    333             # Current path (cwd)
    334             opts.output_file = os.path.join(os.getcwd(), opts.output_file)
    335 
    336         output_dest = open(opts.output_file, 'w')
    337 
    338     else:
    339 
    340         output_dest = sys.stdout
    341 
    342     try:
    343 
    344         failed_context = {}
    345         passed_context = {}
    346 
    347         failed_count = 0
    348         passed_count = 0
    349 
    350         if opts.summary_mode == 2 and len(args) == 1:
    351             opts.summary_mode = 1
    352 
    353         for testsuite in args:
    354 
    355             # Iterate over the provided test list
    356 
    357             context = {}
    358             exec_log = os.path.join(opts.log_dir, '%s-exec.log' % testsuite)
    359             output_log = os.path.join(opts.log_dir, ('%s-output.log'
    360                                                      % testsuite))
    361 
    362             failed_subset = {}
    363 
    364             runtest_file = os.path.join(opts.ltp_dir, 'runtest', testsuite)
    365 
    366             if not opts.postprocess_only:
    367 
    368                 for log in [exec_log, output_log]:
    369                     if os.path.isfile(log):
    370                         os.remove(log)
    371 
    372                 if not os.access(runtest_file, os.R_OK):
    373                     output_dest.write("%s doesn't exist; skipping "
    374                                       "test\n" % runtest_file)
    375                     continue
    376 
    377                 os.system(' '.join([os.path.join(opts.ltp_dir, 'runltp'),
    378                                     runltp_opts, '-f', testsuite,
    379                                     '-l', exec_log, '-o', output_log]))
    380 
    381             try:
    382 
    383                 failed_subset, passed_css, context = \
    384                     parse_ltp_results(exec_log, output_log,
    385                                   verbose=opts.verbose)
    386 
    387             except ResultsParseException as rpe:
    388                 output_dest.write('Error encountered when parsing results for '
    389                                   'test - %s: %s\n' % (testsuite, str(rpe)))
    390                 continue
    391 
    392             failed_count += len(failed_subset)
    393 
    394             failed_subset_context = {}
    395             passed_subset_context = {}
    396 
    397             if opts.verbose:
    398                 failed_subset_context = determine_context(output_log,
    399                                                           testsuite,
    400                                                           failed_subset,
    401                                                           context)
    402             if type(passed_css) == list:
    403 
    404                 passed_count += len(passed_css)
    405 
    406                 if opts.verbose == 2:
    407                     passed_subset_context = determine_context(output_log,
    408                                                               testsuite,
    409                                                               passed_css,
    410                                                               context)
    411 
    412             else:
    413 
    414                 passed_count += passed_css
    415 
    416             if opts.summary_mode == 1:
    417 
    418                 failed_context.update(failed_subset_context)
    419                 passed_context.update(passed_subset_context)
    420 
    421             else:
    422 
    423                 if 1 <= opts.verbose:
    424                     # Print out failed testcases.
    425                     print_context(output_dest,
    426                                   'FAILED TESTCASES for %s' % testsuite,
    427                                   failed_subset_context)
    428 
    429                 if opts.verbose == 2:
    430                     # Print out passed testcases with context.
    431                     print_context(output_dest,
    432                                   'PASSED TESTCASES for %s' % testsuite,
    433                                   passed_subset_context)
    434 
    435                 if opts.summary_mode == 2:
    436                     output_dest.write("""
    437 ========================================
    438 SUMMARY for: %s
    439 ----------------------------------------
    440 PASS - %d
    441 FAIL - %d
    442 ----------------------------------------
    443 """ % (testsuite, passed_count, len(failed_subset)))
    444 
    445         if opts.summary_mode == 1:
    446 
    447             # Print out overall results.
    448 
    449             if 1 <= opts.verbose:
    450                 # Print out failed testcases with context.
    451                 print_context(output_dest, "FAILED TESTCASES", failed_context)
    452 
    453             if opts.verbose == 2:
    454                 # Print out passed testcases with context.
    455                 print_context(output_dest, "PASSED TESTCASES", passed_context)
    456 
    457             output_dest.write("""
    458 ========================================
    459 SUMMARY for tests:
    460 %s
    461 ----------------------------------------
    462 PASS - %d
    463 FAIL - %d
    464 ----------------------------------------
    465 """ % (' '.join(args), passed_count, failed_count))
    466 
    467     finally:
    468 
    469         if output_dest != sys.stdout:
    470 
    471             output_dest.close()
    472 
    473 if __name__ == '__main__':
    474     main()
    475