Home | History | Annotate | Download | only in scripts
      1 #!/usr/bin/python
      2 # Copyright 2015 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 
      6 # pylint: disable-msg=W0311
      7 
      8 from collections import namedtuple
      9 import argparse
     10 import glob
     11 import json
     12 import os
     13 import pprint
     14 import re
     15 import subprocess
     16 
     17 _EXPECTATIONS_DIR = 'expectations'
     18 _AUTOTEST_RESULT_ID_TEMPLATE = 'gs://chromeos-autotest-results/%s-chromeos-test/chromeos*/graphics_dEQP/debug/graphics_dEQP.DEBUG'
     19 #_AUTOTEST_RESULT_TAG_TEMPLATE = 'gs://chromeos-autotest-results/%s/graphics_dEQP/debug/graphics_dEQP.DEBUG'
     20 _AUTOTEST_RESULT_TAG_TEMPLATE = 'gs://chromeos-autotest-results/%s/debug/client.0.DEBUG'
     21 # Use this template for tryjob results:
     22 #_AUTOTEST_RESULT_TEMPLATE = 'gs://chromeos-autotest-results/%s-ihf/*/graphics_dEQP/debug/graphics_dEQP.DEBUG'
     23 _BOARD_REGEX = re.compile(r'ChromeOS BOARD = (.+)')
     24 _CPU_FAMILY_REGEX = re.compile(r'ChromeOS CPU family = (.+)')
     25 _GPU_FAMILY_REGEX = re.compile(r'ChromeOS GPU family = (.+)')
     26 _TEST_FILTER_REGEX = re.compile(r'dEQP test filter = (.+)')
     27 _HASTY_MODE_REGEX = re.compile(r'\'hasty\': \'True\'|Running in hasty mode.')
     28 
     29 #04/23 07:30:21.624 INFO |graphics_d:0240| TestCase: dEQP-GLES3.functional.shaders.operator.unary_operator.bitwise_not.highp_ivec3_vertex
     30 #04/23 07:30:21.840 INFO |graphics_d:0261| Result: Pass
     31 _TEST_RESULT_REGEX = re.compile(r'TestCase: (.+?)$\n.+? Result: (.+?)$',
     32                                 re.MULTILINE)
     33 _HASTY_TEST_RESULT_REGEX = re.compile(
     34     r'\[stdout\] Test case \'(.+?)\'..$\n'
     35     r'.+?\[stdout\]   (Pass|NotSupported|QualityWarning|CompatibilityWarning|'
     36     r'Fail|ResourceError|Crash|Timeout|InternalError|Skipped) \((.+)\)', re.MULTILINE)
     37 Logfile = namedtuple('Logfile', 'job_id name gs_path')
     38 
     39 
     40 def execute(cmd_list):
     41   sproc = subprocess.Popen(cmd_list, stdout=subprocess.PIPE)
     42   return sproc.communicate()[0]
     43 
     44 
     45 def get_metadata(s):
     46   cpu = re.search(_CPU_FAMILY_REGEX, s).group(1)
     47   gpu = re.search(_GPU_FAMILY_REGEX, s).group(1)
     48   board = re.search(_BOARD_REGEX, s).group(1)
     49   filter = re.search(_TEST_FILTER_REGEX, s).group(1)
     50   hasty = False
     51   if re.search(_HASTY_MODE_REGEX, s):
     52     hasty = True
     53   print('Found results from %s for GPU = %s, filter = %s and hasty = %r.' %
     54         (board, gpu, filter, hasty))
     55   return board, gpu, filter, hasty
     56 
     57 
     58 def copy_logs_from_gs_path(autotest_result_path):
     59   logs = []
     60   gs_paths = execute(['gsutil', 'ls', autotest_result_path]).splitlines()
     61   for gs_path in gs_paths:
     62     job_id = gs_path.split('/')[3].split('-')[0]
     63     # DEBUG logs have more information than INFO logs, especially for hasty.
     64     name = os.path.join('logs', job_id + '_graphics_dEQP.DEBUG')
     65     logs.append(Logfile(job_id, name, gs_path))
     66   for log in logs:
     67     execute(['gsutil', 'cp', log.gs_path, log.name])
     68   return logs
     69 
     70 
     71 def get_local_logs():
     72   logs = []
     73   for name in glob.glob(os.path.join('logs', '*_graphics_dEQP.INFO')):
     74     job_id = name.split('_')[0]
     75     logs.append(Logfile(job_id, name, name))
     76   for name in glob.glob(os.path.join('logs', '*_graphics_dEQP.DEBUG')):
     77     job_id = name.split('_')[0]
     78     logs.append(Logfile(job_id, name, name))
     79   return logs
     80 
     81 
     82 def get_all_tests(text):
     83   tests = []
     84   for test, result in re.findall(_TEST_RESULT_REGEX, text):
     85     tests.append((test, result))
     86   for test, result, details in re.findall(_HASTY_TEST_RESULT_REGEX, text):
     87     tests.append((test, result))
     88   return tests
     89 
     90 
     91 def get_not_passing_tests(text):
     92   not_passing = []
     93   for test, result in re.findall(_TEST_RESULT_REGEX, text):
     94     if not (result == 'Pass' or result == 'NotSupported' or result == 'Skipped' or
     95             result == 'QualityWarning' or result == 'CompatibilityWarning'):
     96       not_passing.append((test, result))
     97   for test, result, details in re.findall(_HASTY_TEST_RESULT_REGEX, text):
     98     if result != 'Pass':
     99       not_passing.append((test, result))
    100   return not_passing
    101 
    102 
    103 def load_expectation_dict(json_file):
    104   data = {}
    105   if os.path.isfile(json_file):
    106     print 'Loading file ' + json_file
    107     with open(json_file, 'r') as f:
    108       text = f.read()
    109       data = json.loads(text)
    110   return data
    111 
    112 
    113 def load_expectations(json_file):
    114   data = load_expectation_dict(json_file)
    115   expectations = {}
    116   # Convert from dictionary of lists to dictionary of sets.
    117   for key in data:
    118     expectations[key] = set(data[key])
    119   return expectations
    120 
    121 
    122 def expectation_list_to_dict(tests):
    123   data = {}
    124   tests = list(set(tests))
    125   for test, result in tests:
    126     if data.has_key(result):
    127       new_list = list(set(data[result].append(test)))
    128       data.pop(result)
    129       data[result] = new_list
    130     else:
    131       data[result] = [test]
    132   return data
    133 
    134 
    135 def save_expectation_dict(expectation_path, expectation_dict):
    136   # Clean up obsolete expectations.
    137   for file_name in glob.glob(expectation_path + '.*'):
    138     if not '.hasty.' in file_name or '.hasty' in expectation_path:
    139       os.remove(file_name)
    140   # Dump json for next iteration.
    141   with open(expectation_path + '.json', 'w') as f:
    142     json.dump(expectation_dict,
    143               f,
    144               sort_keys=True,
    145               indent=4,
    146               separators=(',', ': '))
    147   # Dump plain text for autotest.
    148   for key in expectation_dict:
    149     if expectation_dict[key]:
    150       with open(expectation_path + '.' + key, 'w') as f:
    151         for test in expectation_dict[key]:
    152           f.write(test)
    153           f.write('\n')
    154 
    155 
    156 # Figure out duplicates and move them to Flaky result set/list.
    157 def process_flaky(status_dict):
    158   """Figure out duplicates and move them to Flaky result set/list."""
    159   clean_dict = {}
    160   flaky = set([])
    161   if status_dict.has_key('Flaky'):
    162     flaky = status_dict['Flaky']
    163 
    164   # FLaky tests are tests with 2 distinct results.
    165   for key1 in status_dict.keys():
    166     for key2 in status_dict.keys():
    167       if key1 != key2:
    168         flaky |= status_dict[key1] & status_dict[key2]
    169 
    170   # Remove Flaky tests from other status and convert to dict of list.
    171   for key in status_dict.keys():
    172     if key != 'Flaky':
    173       not_flaky = list(status_dict[key] - flaky)
    174       not_flaky.sort()
    175       print 'Number of "%s" is %d.' % (key, len(not_flaky))
    176       clean_dict[key] = not_flaky
    177 
    178   # And finally process flaky list/set.
    179   flaky_list = list(flaky)
    180   flaky_list.sort()
    181   clean_dict['Flaky'] = flaky_list
    182 
    183   return clean_dict
    184 
    185 
    186 def merge_expectation_list(expectation_path, tests):
    187   status_dict = {}
    188   expectation_json = expectation_path + '.json'
    189   if os.access(expectation_json, os.R_OK):
    190     status_dict = load_expectations(expectation_json)
    191   else:
    192     print 'Could not load', expectation_json
    193   for test, result in tests:
    194     if status_dict.has_key(result):
    195       new_set = status_dict[result]
    196       new_set.add(test)
    197       status_dict.pop(result)
    198       status_dict[result] = new_set
    199     else:
    200       status_dict[result] = set([test])
    201   clean_dict = process_flaky(status_dict)
    202   save_expectation_dict(expectation_path, clean_dict)
    203 
    204 
    205 def load_log(name):
    206   """Load test log and clean it from stderr spew."""
    207   with open(name) as f:
    208     lines = f.read().splitlines()
    209   text = ''
    210   for line in lines:
    211     if ('dEQP test filter =' in line or 'ChromeOS BOARD = ' in line or
    212         'ChromeOS CPU family =' in line or 'ChromeOS GPU family =' in line or
    213         'TestCase: ' in line or 'Result: ' in line or
    214         'Test Options: ' in line or 'Running in hasty mode.' in line or
    215         # For hasty logs we have:
    216         'Pass (' in line or 'NotSupported (' in line or 'Skipped (' in line or
    217         'QualityWarning (' in line or 'CompatibilityWarning (' in line or
    218         'Fail (' in line or 'ResourceError (' in line or 'Crash (' in line or
    219         'Timeout (' in line or 'InternalError (' in line or
    220         ' Test case \'' in line):
    221       text += line + '\n'
    222   # TODO(ihf): Warn about or reject log files missing the end marker.
    223   return text
    224 
    225 
    226 def all_passing(tests):
    227   for _, result in tests:
    228     if not (result == 'Pass'):
    229       return False
    230   return True
    231 
    232 
    233 def process_logs(logs):
    234   for log in logs:
    235     text = load_log(log.name)
    236     if text:
    237       print '================================================================'
    238       print 'Loading %s...' % log.name
    239       try:
    240         _, gpu, filter, hasty = get_metadata(text)
    241         tests = get_all_tests(text)
    242         print 'Found %d test results.' % len(tests)
    243         if all_passing(tests):
    244           # Delete logs that don't contain failures.
    245           os.remove(log.name)
    246         else:
    247           # GPU family goes first in path to simplify adding/deleting families.
    248           output_path = os.path.join(_EXPECTATIONS_DIR, gpu)
    249           if not os.access(output_path, os.R_OK):
    250             os.makedirs(output_path)
    251           expectation_path = os.path.join(output_path, filter)
    252           if hasty:
    253             expectation_path = os.path.join(output_path, filter + '.hasty')
    254           merge_expectation_list(expectation_path, tests)
    255       except:
    256         print 'Error processing %s' % log.name
    257 
    258 
    259 JOB_TAGS_ALL = (
    260 'select distinct job_tag from chromeos_autotest_db.tko_test_view_2 '
    261 'where not job_tag like "%%hostless" and '
    262 'test_name LIKE "graphics_dEQP%%" and '
    263 'build_version>="%s" and '
    264 'build_version<="%s" and '
    265 '((status = "FAIL" and not job_name like "%%.NotPass") or '
    266 'job_name like "%%.functional" or '
    267 'job_name like "%%-master")' )
    268 
    269 JOB_TAGS_MASTER = (
    270 'select distinct job_tag from chromeos_autotest_db.tko_test_view_2 '
    271 'where not job_tag like "%%hostless" and '
    272 'test_name LIKE "graphics_dEQP%%" and '
    273 'build_version>="%s" and '
    274 'build_version<="%s" and '
    275 'job_name like "%%-master"' )
    276 
    277 def get_result_paths_from_autotest_db(host, user, password, build_from,
    278                                       build_to):
    279   paths = []
    280   # TODO(ihf): Introduce flag to toggle between JOB_TAGS_ALL and _MASTER.
    281   sql = JOB_TAGS_MASTER % (build_from, build_to)
    282   cmd = ['mysql', '-u%s' % user, '-p%s' % password, '--host', host, '-e', sql]
    283   p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
    284   for line in p.communicate()[0].splitlines():
    285     # Skip over unrelated sql spew (really first line only):
    286     if line and 'chromeos-test' in line:
    287       paths.append(_AUTOTEST_RESULT_TAG_TEMPLATE % line.rstrip())
    288   print 'Found %d potential results in the database.' % len(paths)
    289   return paths
    290 
    291 
    292 def copy_logs_from_gs_paths(paths):
    293   i = 1
    294   for gs_path in paths:
    295     print '[%d/%d] %s' % (i, len(paths), gs_path)
    296     copy_logs_from_gs_path(gs_path)
    297     i = i+1
    298 
    299 
    300 argparser = argparse.ArgumentParser(
    301     description='Download from GS and process dEQP logs into expectations.')
    302 argparser.add_argument(
    303     '--host',
    304     dest='host',
    305     default='173.194.81.83',
    306     help='Host containing autotest result DB.')
    307 argparser.add_argument('--user', dest='user', help='Database user account.')
    308 argparser.add_argument(
    309     '--password',
    310     dest='password',
    311     help='Password for user account.')
    312 argparser.add_argument(
    313     '--from',
    314     dest='build_from',
    315     help='Lowest build revision to include. Example: R51-8100.0.0')
    316 argparser.add_argument(
    317     '--to',
    318     dest='build_to',
    319     help='Highest build revision to include. Example: R51-8101.0.0')
    320 
    321 args = argparser.parse_args()
    322 
    323 print pprint.pformat(args)
    324 # This is somewhat optional. Remove existing expectations to start clean, but
    325 # feel free to process them incrementally.
    326 execute(['rm', '-rf', _EXPECTATIONS_DIR])
    327 
    328 copy_logs_from_gs_paths(get_result_paths_from_autotest_db(
    329     args.host, args.user, args.password, args.build_from, args.build_to))
    330 
    331 # This will include the just downloaded logs from GS as well.
    332 logs = get_local_logs()
    333 process_logs(logs)
    334