Scott Graham | ab0c382 | 2017-11-16 19:08:55 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # Copyright 2017 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 | """Runs all permutations of pairs of tests in a gtest binary to attempt to |
| 7 | detect state leakage between tests. |
| 8 | |
| 9 | Example invocation: |
| 10 | |
| 11 | gn gen out/asan --args='is_asan=true enable_nacl=false is_debug=false' |
| 12 | ninja -C out/asan base_unittests |
| 13 | tools/perry.py out/asan/base_unittests > perry.log & |
| 14 | tail -f perry.log |
| 15 | |
| 16 | You might want to run it in `screen` as it'll take a while. |
| 17 | """ |
| 18 | |
| 19 | import argparse |
| 20 | import os |
| 21 | import multiprocessing |
| 22 | import subprocess |
| 23 | import sys |
| 24 | |
| 25 | |
| 26 | def _GetTestList(path_to_binary): |
| 27 | """Returns a set of full test names. |
| 28 | |
| 29 | Each test will be of the form "Case.Test". There will be a separate line |
| 30 | for each combination of Case/Test (there are often multiple tests in each |
| 31 | case). |
| 32 | """ |
| 33 | raw_output = subprocess.check_output([path_to_binary, "--gtest_list_tests"]) |
| 34 | input_lines = raw_output.splitlines() |
| 35 | |
| 36 | # The format of the gtest_list_tests output is: |
| 37 | # "Case1." |
| 38 | # " Test1 # <Optional extra stuff>" |
| 39 | # " Test2" |
| 40 | # "Case2." |
| 41 | # " Test1" |
| 42 | case_name = '' # Includes trailing dot. |
| 43 | test_set = set() |
| 44 | for line in input_lines: |
| 45 | if len(line) > 1: |
| 46 | if '#' in line: |
| 47 | line = line[:line.find('#')] |
| 48 | if line[0] == ' ': |
| 49 | # Indented means a test in previous case. |
| 50 | test_set.add(case_name + line.strip()) |
| 51 | else: |
| 52 | # New test case. |
| 53 | case_name = line.strip() |
| 54 | |
| 55 | return test_set |
| 56 | |
| 57 | |
| 58 | def _CheckForFailure(data): |
| 59 | test_binary, pair0, pair1 = data |
| 60 | p = subprocess.Popen( |
| 61 | [test_binary, '--gtest_repeat=5', '--gtest_shuffle', |
| 62 | '--gtest_filter=' + pair0 + ':' + pair1], |
| 63 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| 64 | out, _ = p.communicate() |
| 65 | if p.returncode != 0: |
| 66 | return (pair0, pair1, out) |
| 67 | return None |
| 68 | |
| 69 | |
| 70 | def _PrintStatus(i, total, failed): |
| 71 | status = '%d of %d tested (%d failures)' % (i+1, total, failed) |
| 72 | print '\r%s%s' % (status, '\x1B[K'), |
| 73 | sys.stdout.flush() |
| 74 | |
| 75 | |
| 76 | def main(): |
| 77 | parser = argparse.ArgumentParser(description="Find failing pairs of tests.") |
| 78 | parser.add_argument('binary', help='Path to gtest binary or wrapper script.') |
| 79 | args = parser.parse_args() |
| 80 | print 'Getting test list...' |
| 81 | all_tests = _GetTestList(args.binary) |
| 82 | permuted = [(args.binary, x, y) for x in all_tests for y in all_tests] |
| 83 | |
| 84 | failed = [] |
| 85 | pool = multiprocessing.Pool() |
| 86 | total_count = len(permuted) |
| 87 | for i, result in enumerate(pool.imap_unordered( |
| 88 | _CheckForFailure, permuted, 1)): |
| 89 | if result: |
| 90 | print '\n--gtest_filter=%s:%s failed\n\n%s\n\n' % ( |
| 91 | result[0], result[1], result[2]) |
| 92 | failed.append(result) |
| 93 | _PrintStatus(i, total_count, len(failed)) |
| 94 | |
| 95 | pool.terminate() |
| 96 | pool.join() |
| 97 | |
| 98 | if failed: |
| 99 | print 'Failed pairs:' |
| 100 | for f in failed: |
| 101 | print f[0], f[1] |
| 102 | |
| 103 | return 0 |
| 104 | |
| 105 | |
| 106 | if __name__ == '__main__': |
| 107 | sys.exit(main()) |