Index: tools/flakiness/find_flakiness.py |
diff --git a/tools/flakiness/find_flakiness.py b/tools/flakiness/find_flakiness.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..21629e4a7a6dafabcbfe20dea7bdb6bae5f856d2 |
--- /dev/null |
+++ b/tools/flakiness/find_flakiness.py |
@@ -0,0 +1,179 @@ |
+#!/usr/bin/env python |
+# Copyright (c) 2011 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+"""Contains two functions that run different test cases and the same test |
+case in parallel repeatedly to identify flaky tests. |
+""" |
+ |
+ |
+import os |
+import re |
+import subprocess |
+import time |
+ |
+ |
+# Defaults for FindShardingFlakiness(). |
+FF_DATA_SUFFIX = '_flakies' |
+FF_SLEEP_INTERVAL = 10.0 |
+FF_NUM_ITERATIONS = 100 |
+FF_SUPERVISOR_ARGS = ['-r3', '--random-seed'] |
+ |
+# Defaults for FindUnaryFlakiness(). |
+FF_OUTPUT_SUFFIX = '_purges' |
+FF_NUM_PROCS = 20 |
+FF_NUM_REPEATS = 10 |
+FF_TIMEOUT = 600 |
+ |
+ |
+def FindShardingFlakiness(test_path, data_path, supervisor_args): |
+ """Finds flaky test cases by sharding and running a test for the specified |
+ number of times. The data file is read at the beginning of each run to find |
+ the last known counts and is overwritten at the end of each run with the new |
+ counts. There is an optional sleep interval between each run so the script can |
+ be killed without losing the data, useful for overnight (or weekend!) runs. |
+ """ |
+ |
+ failed_tests = {} |
+ # Read a previously written data file. |
+ if os.path.exists(data_path): |
+ data_file = open(data_path, 'r') |
+ num_runs = int(data_file.readline().split(' ')[0]) |
+ num_passes = int(data_file.readline().split(' ')[0]) |
+ for line in data_file: |
+ if line: |
+ split_line = line.split(' -> ') |
+ failed_tests[split_line[0]] = int(split_line[1]) |
+ data_file.close() |
+ # No data file found. |
+ else: |
+ num_runs = 0 |
+ num_passes = 0 |
+ |
+ log_lines = False |
+ args = ['python', '../sharding_supervisor/sharding_supervisor.py'] |
+ args.extend(supervisor_args + [test_path]) |
+ proc = subprocess.Popen(args, stderr=subprocess.PIPE) |
+ |
+ # Shard the test and collect failures. |
+ while True: |
+ line = proc.stderr.readline() |
+ if not line: |
+ if proc.poll() is not None: |
+ break |
+ continue |
+ print line.rstrip() |
+ if log_lines: |
+ line = line.rstrip() |
+ if line in failed_tests: |
+ failed_tests[line] += 1 |
+ else: |
+ failed_tests[line] = 1 |
+ elif line.find('FAILED TESTS:') >= 0: |
+ log_lines = True |
+ num_runs += 1 |
+ if proc.returncode == 0: |
+ num_passes += 1 |
+ |
+ # Write the data file and print results. |
+ data_file = open(data_path, 'w') |
+ print '%i runs' % num_runs |
+ data_file.write('%i runs\n' % num_runs) |
+ print '%i passes' % num_passes |
+ data_file.write('%i passes\n' % num_passes) |
+ for (test, count) in failed_tests.iteritems(): |
+ print '%s -> %i' % (test, count) |
+ data_file.write('%s -> %i\n' % (test, count)) |
+ data_file.close() |
+ |
+ |
+def FindUnaryFlakiness(test_path, output_path, num_procs, num_repeats, timeout): |
+ """Runs all the test cases in a given test in parallel with itself, to get at |
+ those that hold on to shared resources. The idea is that if a test uses a |
+ unary resource, then running many instances of this test will purge out some |
+ of them as failures or timeouts. |
+ """ |
+ |
+ test_name_regex = r'((\w+/)?\w+\.\w+(/\d+)?)' |
+ test_start = re.compile('\[\s+RUN\s+\] ' + test_name_regex) |
+ test_list = [] |
+ |
+ # Run the test to discover all the test cases. |
+ proc = subprocess.Popen([test_path], stdout=subprocess.PIPE) |
+ while True: |
+ line = proc.stdout.readline() |
+ if not line: |
+ if proc.poll() is not None: |
+ break |
+ continue |
+ print line.rstrip() |
+ results = test_start.search(line) |
+ if results: |
+ test_list.append(results.group(1)) |
+ |
+ failures = [] |
+ index = 0 |
+ total = len(test_list) |
+ |
+ # Run each test case in parallel with itself. |
+ for test_name in test_list: |
+ num_fails = 0 |
+ num_terminated = 0 |
+ procs = [] |
+ args = [test_path, '--gtest_filter=' + test_name, |
+ '--gtest_repeat=%i' % num_repeats] |
+ while len(procs) < num_procs: |
+ procs.append(subprocess.Popen(args)) |
+ seconds = 0 |
+ while procs: |
+ for proc in procs: |
+ if proc.poll() is not None: |
+ if proc.returncode != 0: |
+ ++num_fails |
+ procs.remove(proc) |
+ # Timeout exceeded, kill the remaining processes and make a note. |
+ if seconds > timeout: |
+ num_fails += len(procs) |
+ num_terminated = len(procs) |
+ while procs: |
+ procs.pop().terminate() |
+ time.sleep(1.0) |
+ seconds += 1 |
+ if num_fails: |
+ line = '%s: %i failed' % (test_name, num_fails) |
+ if num_terminated: |
+ line += ' (%i terminated)' % num_terminated |
+ failures.append(line) |
+ print '%s (%i / %i): %i failed' % (test_name, index, total, num_fails) |
+ index += 1 |
+ time.sleep(1.0) |
+ |
+ # Print the results and write the data file. |
+ print failures |
+ data_file = open(output_path, 'w') |
+ for line in failures: |
+ data_file.write(line + '\n') |
+ data_file.close() |
+ |
+ |
+def main(): |
+ if not args: |
+ parser.error('You must specify a path to test!') |
+ if not os.path.exists(args[0]): |
+ parser.error('%s does not exist!' % args[0]) |
+ |
+ data_path = os.path.basename(args[0]) + FF_DATA_SUFFIX |
+ output_path = os.path.basename(args[0]) + FF_OUTPUT_SUFFIX |
+ |
+ for i in range(FF_NUM_ITERATIONS): |
+ FindShardingFlakiness(args[0], data_path, FF_SUPERVISOR_ARGS) |
+ print 'That was just iteration %i of %i.' % (i + 1, FF_NUM_ITERATIONS) |
+ time.sleep(FF_SLEEP_INTERVAL) |
+ |
+ FindUnaryFlakiness( |
+ args[0], output_path, FF_NUM_PROCS, FF_NUM_REPEATS, FF_TIMEOUT) |
+ |
+ |
+if __name__ == '__main__': |
+ main() |