OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2014 The Chromium Authors. All rights reserved. | 2 # Copyright 2014 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Runs a test repeatedly to measure its flakiness. The return code is non-zero | 6 """Runs a test repeatedly to measure its flakiness. The return code is non-zero |
7 if the failure rate is higher than the specified threshold, but is not 100%.""" | 7 if the failure rate is higher than the specified threshold, but is not 100%.""" |
8 | 8 |
9 import argparse | 9 import argparse |
10 import multiprocessing.dummy | |
10 import subprocess | 11 import subprocess |
11 import sys | 12 import sys |
12 import time | 13 import time |
13 | 14 |
14 def load_options(): | 15 def load_options(): |
15 parser = argparse.ArgumentParser(description=__doc__) | 16 parser = argparse.ArgumentParser(description=__doc__) |
16 parser.add_argument('--retries', default=1000, type=int, | 17 parser.add_argument('--retries', default=1000, type=int, |
17 help='Number of test retries to measure flakiness.') | 18 help='Number of test retries to measure flakiness.') |
18 parser.add_argument('--threshold', default=0.05, type=float, | 19 parser.add_argument('--threshold', default=0.05, type=float, |
19 help='Minimum flakiness level at which test is ' | 20 help='Minimum flakiness level at which test is ' |
20 'considered flaky.') | 21 'considered flaky.') |
21 parser.add_argument('--jobs', '-j', type=int, default=1, | 22 parser.add_argument('--jobs', '-j', type=int, default=1, |
22 help='Number of parallel jobs to run tests.') | 23 help='Number of parallel jobs to run tests.') |
23 parser.add_argument('command', nargs='+', help='Command to run test.') | 24 parser.add_argument('command', nargs='+', help='Command to run test.') |
24 return parser.parse_args() | 25 return parser.parse_args() |
25 | 26 |
26 | 27 def run_test(job): |
27 def process_finished(running, num_passed, num_failed): | 28 print 'Starting retry attempt %d out of %d' % (job['index'] + 1, |
28 finished = [p for p in running if p.poll() is not None] | 29 job['retries']) |
29 running[:] = [p for p in running if p.poll() is None] | 30 return subprocess.check_call(job['cmd'], stdout=subprocess.PIPE, |
30 num_passed += len([p for p in finished if p.returncode == 0]) | 31 stderr=subprocess.STDOUT) |
31 num_failed += len([p for p in finished if p.returncode != 0]) | |
32 print '%d processed finished. Total passed: %d. Total failed: %d' % ( | |
33 len(finished), num_passed, num_failed) | |
34 return num_passed, num_failed | |
35 | |
36 | 32 |
37 def main(): | 33 def main(): |
38 options = load_options() | 34 options = load_options() |
39 num_passed = num_failed = 0 | 35 num_passed = num_failed = 0 |
40 running = [] | 36 running = [] |
41 | 37 |
42 # Start all retries, while limiting total number of running processes. | 38 pool = multiprocessing.dummy.Pool(processes=options.jobs) |
43 for attempt in range(options.retries): | 39 args = [{'index': index, 'retries': options.retries, 'cmd': options.command} |
44 print 'Starting retry %d out of %d\n' % (attempt + 1, options.retries) | 40 for index in range(options.retries)] |
45 running.append(subprocess.Popen(options.command, stdout=subprocess.PIPE, | 41 results = pool.map(run_test, args) |
46 stderr=subprocess.STDOUT)) | 42 num_passed = len([retcode for retcode in results if retcode == 0]) |
47 while len(running) >= options.jobs: | 43 num_failed = len(results) - num_passed |
48 print 'Waiting for previous retries to finish before starting new ones...' | |
49 time.sleep(0.1) | |
50 num_passed, num_failed = process_finished(running, num_passed, num_failed) | |
51 | 44 |
52 | |
53 # Wait for the remaining retries to finish. | |
54 print 'Waiting for the remaining retries to finish...' | |
55 for process in running: | |
56 process.wait() | |
57 | |
58 num_passed, num_failed = process_finished(running, num_passed, num_failed) | |
59 if num_passed == 0 or num_failed == 0: | 45 if num_passed == 0 or num_failed == 0: |
qyearsley
2014/09/16 18:52:09
You could also omit `or num_failed == 0`, since it
Sergiy Byelozyorov
2014/09/17 15:49:33
Done.
| |
60 flakiness = 0 | 46 flakiness = 0 |
61 else: | 47 else: |
62 flakiness = num_failed / float(options.retries) | 48 flakiness = num_failed / float(len(results)) |
63 | 49 |
64 print 'Flakiness is %.2f' % flakiness | 50 print 'Flakiness is %.2f' % flakiness |
65 if flakiness > options.threshold: | 51 if flakiness > options.threshold: |
66 return 1 | 52 return 1 |
67 else: | 53 else: |
68 return 0 | 54 return 0 |
69 | 55 |
70 | 56 |
71 if __name__ == '__main__': | 57 if __name__ == '__main__': |
72 sys.exit(main()) | 58 sys.exit(main()) |
OLD | NEW |