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