Home | History | Annotate | Download | only in flakiness
      1 #!/usr/bin/env python
      2 # Copyright (c) 2011 The Chromium 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 """Contains two functions that run different test cases and the same test
      7 case in parallel repeatedly to identify flaky tests.
      8 """
      9 
     10 
     11 import os
     12 import re
     13 import subprocess
     14 import time
     15 
     16 
     17 # Defaults for FindShardingFlakiness().
     18 FF_DATA_SUFFIX = '_flakies'
     19 FF_SLEEP_INTERVAL = 10.0
     20 FF_NUM_ITERATIONS = 100
     21 FF_SUPERVISOR_ARGS = ['-r3', '--random-seed']
     22 
     23 # Defaults for FindUnaryFlakiness().
     24 FF_OUTPUT_SUFFIX = '_purges'
     25 FF_NUM_PROCS = 20
     26 FF_NUM_REPEATS = 10
     27 FF_TIMEOUT = 600
     28 
     29 
     30 def FindShardingFlakiness(test_path, data_path, supervisor_args):
     31   """Finds flaky test cases by sharding and running a test for the specified
     32   number of times. The data file is read at the beginning of each run to find
     33   the last known counts and is overwritten at the end of each run with the new
     34   counts. There is an optional sleep interval between each run so the script can
     35   be killed without losing the data, useful for overnight (or weekend!) runs.
     36   """
     37 
     38   failed_tests = {}
     39   # Read a previously written data file.
     40   if os.path.exists(data_path):
     41     data_file = open(data_path, 'r')
     42     num_runs = int(data_file.readline().split(' ')[0])
     43     num_passes = int(data_file.readline().split(' ')[0])
     44     for line in data_file:
     45       if line:
     46         split_line = line.split(' -> ')
     47         failed_tests[split_line[0]] = int(split_line[1])
     48     data_file.close()
     49   # No data file found.
     50   else:
     51     num_runs = 0
     52     num_passes = 0
     53 
     54   log_lines = False
     55   args = ['python', '../sharding_supervisor/sharding_supervisor.py']
     56   args.extend(supervisor_args + [test_path])
     57   proc = subprocess.Popen(args, stderr=subprocess.PIPE)
     58 
     59   # Shard the test and collect failures.
     60   while True:
     61     line = proc.stderr.readline()
     62     if not line:
     63       if proc.poll() is not None:
     64         break
     65       continue
     66     print line.rstrip()
     67     if log_lines:
     68       line = line.rstrip()
     69       if line in failed_tests:
     70         failed_tests[line] += 1
     71       else:
     72         failed_tests[line] = 1
     73     elif line.find('FAILED TESTS:') >= 0:
     74       log_lines = True
     75   num_runs += 1
     76   if proc.returncode == 0:
     77     num_passes += 1
     78 
     79   # Write the data file and print results.
     80   data_file = open(data_path, 'w')
     81   print '%i runs' % num_runs
     82   data_file.write('%i runs\n' % num_runs)
     83   print '%i passes' % num_passes
     84   data_file.write('%i passes\n' % num_passes)
     85   for (test, count) in failed_tests.iteritems():
     86     print '%s -> %i' % (test, count)
     87     data_file.write('%s -> %i\n' % (test, count))
     88   data_file.close()
     89 
     90 
     91 def FindUnaryFlakiness(test_path, output_path, num_procs, num_repeats, timeout):
     92   """Runs all the test cases in a given test in parallel with itself, to get at
     93   those that hold on to shared resources. The idea is that if a test uses a
     94   unary resource, then running many instances of this test will purge out some
     95   of them as failures or timeouts.
     96   """
     97 
     98   test_name_regex = r'((\w+/)?\w+\.\w+(/\d+)?)'
     99   test_start = re.compile('\[\s+RUN\s+\] ' + test_name_regex)
    100   test_list = []
    101 
    102   # Run the test to discover all the test cases.
    103   proc = subprocess.Popen([test_path], stdout=subprocess.PIPE)
    104   while True:
    105     line = proc.stdout.readline()
    106     if not line:
    107       if proc.poll() is not None:
    108         break
    109       continue
    110     print line.rstrip()
    111     results = test_start.search(line)
    112     if results:
    113       test_list.append(results.group(1))
    114 
    115   failures = []
    116   index = 0
    117   total = len(test_list)
    118 
    119   # Run each test case in parallel with itself.
    120   for test_name in test_list:
    121     num_fails = 0
    122     num_terminated = 0
    123     procs = []
    124     args = [test_path, '--gtest_filter=' + test_name,
    125             '--gtest_repeat=%i' % num_repeats]
    126     while len(procs) < num_procs:
    127       procs.append(subprocess.Popen(args))
    128     seconds = 0
    129     while procs:
    130       for proc in procs:
    131         if proc.poll() is not None:
    132           if proc.returncode != 0:
    133             ++num_fails
    134           procs.remove(proc)
    135       # Timeout exceeded, kill the remaining processes and make a note.
    136       if seconds > timeout:
    137         num_fails += len(procs)
    138         num_terminated = len(procs)
    139         while procs:
    140           procs.pop().terminate()
    141       time.sleep(1.0)
    142       seconds += 1
    143     if num_fails:
    144       line = '%s: %i failed' % (test_name, num_fails)
    145       if num_terminated:
    146         line += ' (%i terminated)' % num_terminated
    147       failures.append(line)
    148     print '%s (%i / %i): %i failed' % (test_name, index, total, num_fails)
    149     index += 1
    150     time.sleep(1.0)
    151 
    152   # Print the results and write the data file.
    153   print failures
    154   data_file = open(output_path, 'w')
    155   for line in failures:
    156     data_file.write(line + '\n')
    157   data_file.close()
    158 
    159 
    160 def main():
    161   if not args:
    162     parser.error('You must specify a path to test!')
    163   if not os.path.exists(args[0]):
    164     parser.error('%s does not exist!' % args[0])
    165 
    166   data_path = os.path.basename(args[0]) + FF_DATA_SUFFIX
    167   output_path = os.path.basename(args[0]) + FF_OUTPUT_SUFFIX
    168 
    169   for i in range(FF_NUM_ITERATIONS):
    170     FindShardingFlakiness(args[0], data_path, FF_SUPERVISOR_ARGS)
    171     print 'That was just iteration %i of %i.' % (i + 1, FF_NUM_ITERATIONS)
    172     time.sleep(FF_SLEEP_INTERVAL)
    173 
    174   FindUnaryFlakiness(
    175       args[0], output_path, FF_NUM_PROCS, FF_NUM_REPEATS, FF_TIMEOUT)
    176 
    177 
    178 if __name__ == '__main__':
    179   main()
    180