OLD | NEW |
---|---|
1 #!/usr/bin/env python | |
2 # Copyright 2014 The Chromium Authors. All rights reserved. | 1 # Copyright 2014 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 3 # found in the LICENSE file. |
5 | 4 |
6 """Runs a test repeatedly to measure its flakiness. The return code is non-zero | 5 """Unit tests for is_flaky.""" |
7 if the failure rate is higher than the specified threshold, but is not 100%.""" | |
8 | 6 |
9 import argparse | 7 import is_flaky |
10 import subprocess | 8 import subprocess |
11 import sys | 9 import sys |
12 import time | 10 import threading |
13 | 11 import unittest |
14 def load_options(): | |
15 parser = argparse.ArgumentParser(description=__doc__) | |
16 parser.add_argument('--retries', default=1000, type=int, | |
17 help='Number of test retries to measure flakiness.') | |
18 parser.add_argument('--threshold', default=0.05, type=float, | |
19 help='Minimum flakiness level at which test is ' | |
20 'considered flaky.') | |
21 parser.add_argument('--jobs', '-j', type=int, default=1, | |
22 help='Number of parallel jobs to run tests.') | |
23 parser.add_argument('command', nargs='+', help='Command to run test.') | |
24 return parser.parse_args() | |
25 | 12 |
26 | 13 |
27 def process_finished(running, num_passed, num_failed): | 14 class IsFlakyTest(unittest.TestCase): |
28 finished = [p for p in running if p.poll() is not None] | 15 """Tests for the is_flaky.py functions.""" |
qyearsley
2014/09/16 18:52:09
Ojan might think that this docstring is redundant/
Sergiy Byelozyorov
2014/09/17 15:49:33
Done.
| |
29 running[:] = [p for p in running if p.poll() is None] | |
30 num_passed += len([p for p in finished if p.returncode == 0]) | |
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 | 16 |
17 def mock_check_call(self, command, stdout, stderr): | |
18 self.check_call_calls.append(command) | |
19 if self.check_call_results: | |
20 return self.check_call_results.pop(0) | |
21 else: | |
22 return 0 | |
36 | 23 |
37 def main(): | 24 def mock_load_options(self): |
38 options = load_options() | 25 class MockOptions(): |
39 num_passed = num_failed = 0 | 26 jobs = 2 |
40 running = [] | 27 retries = 10 |
28 threshold = 0.3 | |
29 command = ['command', 'param1', 'param2'] | |
30 return MockOptions() | |
41 | 31 |
42 # Start all retries, while limiting total number of running processes. | 32 def setUp(self): |
43 for attempt in range(options.retries): | 33 self.original_subprocess_check_call = subprocess.check_call |
44 print 'Starting retry %d out of %d\n' % (attempt + 1, options.retries) | 34 subprocess.check_call = self.mock_check_call |
qyearsley
2014/09/16 18:52:09
An alternative to this way is using the "mock" mod
Sergiy Byelozyorov
2014/09/17 15:49:33
I'd love that, but mock module is not installed by
| |
45 running.append(subprocess.Popen(options.command, stdout=subprocess.PIPE, | 35 self.check_call_calls = [] |
46 stderr=subprocess.STDOUT)) | 36 self.check_call_results = [] |
47 while len(running) >= options.jobs: | 37 is_flaky.load_options = self.mock_load_options |
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 | 38 |
39 def tearDown(self): | |
40 subprocess.check_call = self.original_subprocess_check_call | |
qyearsley
2014/09/16 18:52:09
setUp and tearDown could also be put at the top of
Sergiy Byelozyorov
2014/09/17 15:49:34
Done.
| |
52 | 41 |
53 # Wait for the remaining retries to finish. | 42 def testExecutesTestCorrectNumberOfTimes(self): |
54 print 'Waiting for the remaining retries to finish...' | 43 is_flaky.main() |
55 for process in running: | 44 self.assertEqual(len(self.check_call_calls), 10) |
56 process.wait() | |
57 | 45 |
58 num_passed, num_failed = process_finished(running, num_passed, num_failed) | 46 def testExecutesTestWithCorrectArguments(self): |
59 if num_passed == 0 or num_failed == 0: | 47 is_flaky.main() |
60 flakiness = 0 | 48 for call in self.check_call_calls: |
61 else: | 49 self.assertEqual(call, ['command', 'param1', 'param2']) |
62 flakiness = num_failed / float(options.retries) | |
63 | 50 |
64 print 'Flakiness is %.2f' % flakiness | 51 def testReturnsNonFlakyForAllSuccesses(self): |
65 if flakiness > options.threshold: | 52 self.check_call_results = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
66 return 1 | 53 ret_code = is_flaky.main() |
67 else: | 54 self.assertEqual(ret_code, 0) |
68 return 0 | 55 |
56 def testReturnsNonFlakyForAllFailures(self): | |
57 self.check_call_results = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] | |
58 ret_code = is_flaky.main() | |
59 self.assertEqual(ret_code, 0) | |
60 | |
61 def testReturnsNonFlakyForSmallNumberOfFailures(self): | |
62 self.check_call_results = [1, 0, 1, 0, 0, 0, 0, 0, 0, 0] | |
63 ret_code = is_flaky.main() | |
64 self.assertEqual(ret_code, 0) | |
qyearsley
2014/09/16 18:52:09
You might note that this failure rate of 2/10 is l
Sergiy Byelozyorov
2014/09/17 15:49:34
That's right. That's why the return code is 0 - te
| |
65 | |
66 def testReturnsFlakyForLargeNumberOfFailures(self): | |
67 self.check_call_results = [1, 1, 1, 0, 1, 0, 0, 0, 0, 0] | |
68 ret_code = is_flaky.main() | |
69 self.assertEqual(ret_code, 1) | |
69 | 70 |
70 | 71 |
71 if __name__ == '__main__': | 72 if __name__ == '__main__': |
72 sys.exit(main()) | 73 unittest.main() |
OLD | NEW |