Home | History | Annotate | Download | only in graphics_PiglitBVT
      1 #!/usr/bin/python
      2 # Copyright 2014 The Chromium OS Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 from __future__ import print_function
      6 from collections import namedtuple
      7 import json, os, re, sys
      8 
      9 AUTOTEST_NAME = 'graphics_PiglitBVT'
     10 INPUT_DIR = './piglit_logs/'
     11 OUTPUT_DIR = './test_scripts/'
     12 OUTPUT_FILE_PATTERN = OUTPUT_DIR + '/%s/' + AUTOTEST_NAME + '_%d.sh'
     13 OUTPUT_FILE_SLICES = 20
     14 PIGLIT_PATH = '/usr/local/piglit/lib/piglit/'
     15 PIGLIT64_PATH = '/usr/local/piglit/lib64/piglit/'
     16 
     17 # Do not generate scripts with "bash -e" as we want to handle errors ourself.
     18 FILE_HEADER = '#!/bin/bash\n\n'
     19 
     20 # Script fragment function that kicks off individual piglit tests.
     21 FILE_RUN_TEST = '\n\
     22 function run_test()\n\
     23 {\n\
     24   local name="$1"\n\
     25   local time="$2"\n\
     26   local command="$3"\n\
     27   echo "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"\n\
     28   echo "+ Running test [$name] of expected runtime $time sec: [$command]"\n\
     29   sync\n\
     30   $command\n\
     31   if [ $? == 0 ] ; then\n\
     32     let "need_pass--"\n\
     33     echo "+ pass :: $name"\n\
     34   else\n\
     35     let "failures++"\n\
     36     echo "+ fail :: $name"\n\
     37   fi\n\
     38 }\n\
     39 '
     40 
     41 # Script fragment that sumarizes the overall status.
     42 FILE_SUMMARY = 'popd\n\
     43 \n\
     44 if [ $need_pass == 0 ] ; then\n\
     45   echo "+---------------------------------------------+"\n\
     46   echo "| Overall pass, as all %d tests have passed. |"\n\
     47   echo "+---------------------------------------------+"\n\
     48 else\n\
     49   echo "+-----------------------------------------------------------+"\n\
     50   echo "| Overall failure, as $need_pass tests did not pass and $failures failed. |"\n\
     51   echo "+-----------------------------------------------------------+"\n\
     52 fi\n\
     53 exit $need_pass\n\
     54 '
     55 
     56 # Control file template for executing a slice.
     57 CONTROL_FILE = "\
     58 # Copyright 2014 The Chromium OS Authors. All rights reserved.\n\
     59 # Use of this source code is governed by a BSD-style license that can be\n\
     60 # found in the LICENSE file.\n\
     61 \n\
     62 NAME = '" + AUTOTEST_NAME + "'\n\
     63 AUTHOR = 'chromeos-gfx'\n\
     64 PURPOSE = 'Collection of automated tests for OpenGL implementations.'\n\
     65 CRITERIA = 'All tests in a slice have to pass, otherwise it will fail.'\n\
     66 TIME='SHORT'\n\
     67 TEST_CATEGORY = 'Functional'\n\
     68 TEST_CLASS = 'graphics'\n\
     69 TEST_TYPE = 'client'\n\
     70 JOB_RETRIES = 2\n\
     71 \n\
     72 BUG_TEMPLATE = {\n\
     73     'labels': ['Cr-OS-Kernel-Graphics'],\n\
     74 }\n\
     75 \n\
     76 DOC = \"\"\"\n\
     77 Piglit is a collection of automated tests for OpenGL implementations.\n\
     78 \n\
     79 The goal of Piglit is to help improve the quality of open source OpenGL drivers\n\
     80 by providing developers with a simple means to perform regression tests.\n\
     81 \n\
     82 This control file runs slice %d out of %d slices of a passing subset of the\n\
     83 original collection.\n\
     84 \n\
     85 http://piglit.freedesktop.org\n\
     86 \"\"\"\n\
     87 \n\
     88 job.run_test('" + AUTOTEST_NAME + "', test_slice=%d)\
     89 "
     90 
     91 def output_control_file(sl, slices):
     92   """
     93   Write control file for slice sl to disk.
     94   """
     95   filename = 'control.%d' % sl
     96   with open(filename, 'w+') as f:
     97     print(CONTROL_FILE % (sl, slices, sl), file=f)
     98 
     99 
    100 def append_script_header(f, need_pass, piglit_path):
    101   """
    102   Write the beginning of the test script to f.
    103   """
    104   print(FILE_HEADER, file=f)
    105   # need_pass is the script variable that counts down to zero and gets returned.
    106   print('need_pass=%d' % need_pass, file=f)
    107   print('failures=0', file=f)
    108   print('PIGLIT_PATH=%s' % piglit_path, file=f)
    109   print('export PIGLIT_SOURCE_DIR=%s' % piglit_path, file=f)
    110   print('export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PIGLIT_PATH/lib', file=f)
    111   print('export DISPLAY=:0', file=f)
    112   print('export XAUTHORITY=/home/chronos/.Xauthority', file=f)
    113   print('', file=f)
    114   print(FILE_RUN_TEST, file=f)
    115   print('', file=f)
    116   print('pushd $PIGLIT_PATH', file=f)
    117 
    118 
    119 def append_script_summary(f, need_pass):
    120   """
    121   Append the summary to the test script f with a required pass count.
    122   """
    123   print(FILE_SUMMARY % need_pass, file=f)
    124 
    125 
    126 def mkdir_p(path):
    127   """
    128   Create all directories in path.
    129   """
    130   try:
    131     os.makedirs(path)
    132   except OSError:
    133     if os.path.isdir(path):
    134       pass
    135     else:
    136       raise
    137 
    138 def get_filepaths(family_root, regex):
    139   """
    140   Find all files that were placed into family_root.
    141   Used to find regular log files (*results.json) and expectations*.json.
    142   """
    143   main_files = []
    144   for root, _, files in os.walk(family_root):
    145     for filename in files:
    146       if re.search(regex, filename):
    147         main_files.append(os.path.join(root, filename))
    148   return main_files
    149 
    150 
    151 def load_files(main_files):
    152   """
    153   The log files are just python dictionaries, load them from disk.
    154   """
    155   d = {}
    156   for main_file in main_files:
    157     d[main_file] = json.loads(open(main_file).read())
    158   return d
    159 
    160 
    161 # Define a Test data structure containing the command line and runtime.
    162 Test = namedtuple('Test', 'command time passing_count not_passing_count')
    163 
    164 def get_test_statistics(log_dict):
    165   """
    166   Figures out for each test how often is passed/failed, the command line and
    167   how long it runs.
    168   """
    169   statistics = {}
    170   for main_file in log_dict:
    171     for test in log_dict[main_file]['tests']:
    172       # Initialize for all known test names to zero stats.
    173       statistics[test] = Test(None, 0.0, 0, 0)
    174 
    175   for main_file in log_dict:
    176     print('Updating statistics from %s.' % main_file, file=sys.stderr)
    177     tests = log_dict[main_file]['tests']
    178     for test in tests:
    179       command = statistics[test].command
    180       # Verify that each board uses the same command.
    181       if 'command' in tests[test]:
    182         if command:
    183           assert(command == tests[test]['command'])
    184         else:
    185           command = tests[test]['command']
    186       # Bump counts.
    187       if tests[test]['result'] == 'pass':
    188         statistics[test] = Test(command,
    189                                 max(tests[test]['time'],
    190                                     statistics[test].time),
    191                                 statistics[test].passing_count + 1,
    192                                 statistics[test].not_passing_count)
    193       else:
    194         statistics[test] = Test(command,
    195                                 statistics[test].time,
    196                                 statistics[test].passing_count,
    197                                 statistics[test].not_passing_count + 1)
    198 
    199   return statistics
    200 
    201 
    202 def get_max_passing(statistics):
    203   """
    204   Gets the maximum count of passes a test has.
    205   """
    206   max_passing_count = 0
    207   for test in statistics:
    208     max_passing_count = max(statistics[test].passing_count, max_passing_count)
    209   return max_passing_count
    210 
    211 
    212 def get_passing_tests(statistics, expectations):
    213   """
    214   Gets a list of all tests that never failed and have a maximum pass count.
    215   """
    216   tests = []
    217   max_passing_count = get_max_passing(statistics)
    218   for test in statistics:
    219     if (statistics[test].passing_count == max_passing_count and
    220         statistics[test].not_passing_count == 0):
    221       if test not in expectations:
    222         tests.append(test)
    223   return sorted(tests)
    224 
    225 
    226 def get_intermittent_tests(statistics):
    227   """
    228   Gets tests that failed at least once and passed at least once.
    229   """
    230   tests = []
    231   max_passing_count = get_max_passing(statistics)
    232   for test in statistics:
    233     if (statistics[test].passing_count > 0 and
    234         statistics[test].passing_count < max_passing_count and
    235         statistics[test].not_passing_count > 0):
    236       tests.append(test)
    237   return sorted(tests)
    238 
    239 
    240 def cleanup_command(cmd, piglit_path):
    241   """
    242   Make script less location dependent by stripping path from commands.
    243   """
    244   cmd = cmd.replace(piglit_path, '')
    245   cmd = cmd.replace('framework/../', '')
    246   cmd = cmd.replace('tests/../', '')
    247   return cmd
    248 
    249 def process_gpu_family(family, family_root):
    250   """
    251   This takes a directory with log files from the same gpu family and processes
    252   the result log into |slices| runable scripts.
    253   """
    254   print('--> Processing "%s".' % family, file=sys.stderr)
    255   piglit_path = PIGLIT_PATH
    256   if family == 'other':
    257     piglit_path = PIGLIT64_PATH
    258 
    259   log_dict = load_files(get_filepaths(family_root, 'results\.json$'))
    260   # Load all expectations but ignore suggested.
    261   exp_dict = load_files(get_filepaths(family_root, 'expectations.*\.json$'))
    262   statistics = get_test_statistics(log_dict)
    263   expectations = compute_expectations(exp_dict, statistics, family, piglit_path)
    264   # Try to help the person updating piglit by collecting the variance
    265   # across different log files into one expectations file per family.
    266   output_suggested_expectations(expectations, family, family_root)
    267 
    268   # Now start computing the new test scripts.
    269   passing_tests = get_passing_tests(statistics, expectations)
    270 
    271   slices = OUTPUT_FILE_SLICES
    272   current_slice = 1
    273   slice_tests = []
    274   time_slice = 0
    275   num_processed = 0
    276   num_pass_total = len(passing_tests)
    277   time_total = 0
    278   for test in passing_tests:
    279     time_total += statistics[test].time
    280 
    281   # Generate one script containing all tests. This can be used as a simpler way
    282   # to run everything, but also to have an easier diff when updating piglit.
    283   filename = OUTPUT_FILE_PATTERN % (family, 0)
    284   # Ensure the output directory for this family exists.
    285   mkdir_p(os.path.dirname(os.path.realpath(filename)))
    286   if passing_tests:
    287     with open(filename, 'w+') as f:
    288       append_script_header(f, num_pass_total, piglit_path)
    289       for test in passing_tests:
    290         cmd = cleanup_command(statistics[test].command, piglit_path)
    291         time_test = statistics[test].time
    292         print('run_test "%s" %.1f "%s"' % (test, 0.0, cmd), file=f)
    293       append_script_summary(f, num_pass_total)
    294 
    295   # Slice passing tests into several pieces to get below BVT's 20 minute limit.
    296   # TODO(ihf): If we ever get into the situation that one test takes more than
    297   # time_total / slice we would get an empty slice afterward. Fortunately the
    298   # stderr spew should warn the operator of this.
    299   for test in passing_tests:
    300     # We are still writing all the tests that belong in the current slice.
    301     if time_slice < time_total / slices:
    302       slice_tests.append(test)
    303       time_test = statistics[test].time
    304       time_slice += time_test
    305       num_processed += 1
    306 
    307     # We finished the slice. Now output the file with all tests in this slice.
    308     if time_slice >= time_total / slices or num_processed == num_pass_total:
    309       filename = OUTPUT_FILE_PATTERN % (family, current_slice)
    310       with open(filename, 'w+') as f:
    311         need_pass = len(slice_tests)
    312         append_script_header(f, need_pass, piglit_path)
    313         for test in slice_tests:
    314           # Make script less location dependent by stripping path from commands.
    315           cmd = cleanup_command(statistics[test].command, piglit_path)
    316           time_test = statistics[test].time
    317           # TODO(ihf): Pass proper time_test instead of 0.0 once we can use it.
    318           print('run_test "%s" %.1f "%s"'
    319                 % (test, 0.0, cmd), file=f)
    320         append_script_summary(f, need_pass)
    321         output_control_file(current_slice, slices)
    322 
    323       print('Slice %d: max runtime for %d passing tests is %.1f seconds.'
    324             % (current_slice, need_pass, time_slice), file=sys.stderr)
    325       current_slice += 1
    326       slice_tests = []
    327       time_slice = 0
    328 
    329   print('Total max runtime on "%s" for %d passing tests is %.1f seconds.' %
    330           (family, num_pass_total, time_total), file=sys.stderr)
    331 
    332 
    333 def insert_expectation(expectations, test, expectation):
    334   """
    335   Insert test with expectation into expectations directory.
    336   """
    337   if not test in expectations:
    338     # Just copy the whole expectation.
    339     expectations[test] = expectation
    340   else:
    341     # Copy over known fields one at a time but don't overwrite existing.
    342     expectations[test]['result'] = expectation['result']
    343     if (not 'crbug' in expectations[test] and 'crbug' in expectation):
    344       expectations[test]['crbug'] = expectation['crbug']
    345     if (not 'comment' in expectations[test] and 'comment' in expectation):
    346       expectations[test]['comment'] = expectation['comment']
    347     if (not 'command' in expectations[test] and 'command' in expectation):
    348       expectations[test]['command'] = expectation['command']
    349     if (not 'pass rate' in expectations[test] and 'pass rate' in expectation):
    350       expectations[test]['pass rate'] = expectation['pass rate']
    351 
    352 
    353 def compute_expectations(exp_dict, statistics, family, piglit_path):
    354   """
    355   Analyze intermittency and output suggested test expectations.
    356   The suggested test expectation
    357   Test expectations are dictionaries with roughly the same structure as logs.
    358   """
    359   flaky_tests = get_intermittent_tests(statistics)
    360   print('Encountered %d tests that do not always pass in "%s" logs.' %
    361         (len(flaky_tests), family), file=sys.stderr)
    362 
    363   max_passing = get_max_passing(statistics)
    364   expectations = {}
    365   # Merge exp_dict which we loaded from disk into new expectations.
    366   for filename in exp_dict:
    367     for test in exp_dict[filename]['tests']:
    368       expectation = exp_dict[filename]['tests'][test]
    369       # Historic results not considered flaky as pass rate makes no sense
    370       # without current logs.
    371       expectation['result'] = 'skip'
    372       if 'pass rate' in expectation:
    373         expectation.pop('pass rate')
    374       # Overwrite historic commands with recently observed ones.
    375       if test in statistics:
    376         expectation['command'] = cleanup_command(statistics[test].command,
    377                                                  piglit_path)
    378         insert_expectation(expectations, test, expectation)
    379       else:
    380         print ('Historic test [%s] not found in new logs. '
    381                'Dropping it from expectations.' % test, file=sys.stderr)
    382 
    383   # Handle the computed flakiness from the result logs that we just processed.
    384   for test in flaky_tests:
    385     pass_rate = statistics[test].passing_count / float(max_passing)
    386     command = statistics[test].command
    387     # Loading a json converts everything to string anyways, so save it as such
    388     # and make it only 2 significiant digits.
    389     expectation = {'result': 'flaky',
    390                    'pass rate': '%.2f' % pass_rate,
    391                    'command': command}
    392     insert_expectation(expectations, test, expectation)
    393 
    394   return expectations
    395 
    396 
    397 def output_suggested_expectations(expectations, family, family_root):
    398   filename = os.path.join(family_root,
    399                           'suggested_exp_to_rename_%s.json' % family)
    400   with open(filename, 'w+') as f:
    401     json.dump({'tests': expectations}, f, indent=2, sort_keys=True,
    402               separators=(',', ': '))
    403 
    404 
    405 def get_gpu_families(root):
    406   """
    407   We consider each directory under root a possible gpu family.
    408   """
    409   files = os.listdir(root)
    410   families = []
    411   for f in files:
    412     if os.path.isdir(os.path.join(root, f)):
    413       families.append(f)
    414   return families
    415 
    416 
    417 def generate_scripts(root):
    418   """
    419   For each family under root create the corresponding set of passing test
    420   scripts.
    421   """
    422   families = get_gpu_families(root)
    423   for family in families:
    424     process_gpu_family(family, os.path.join(root, family))
    425 
    426 
    427 # We check the log files in as highly compressed binaries.
    428 print('Uncompressing log files...', file=sys.stderr)
    429 os.system('bunzip2 ' + INPUT_DIR + '/*/*/*results.json.bz2')
    430 
    431 # Generate the scripts.
    432 generate_scripts(INPUT_DIR)
    433 
    434 # Binary should remain the same, otherwise use
    435 #   git checkout -- piglit_output
    436 # or similar to reverse.
    437 print('Recompressing log files...', file=sys.stderr)
    438 os.system('bzip2 -9 ' + INPUT_DIR + '/*/*/*results.json')
    439