OLD | NEW |
1 # Copyright 2012 The Chromium Authors. All rights reserved. | 1 # Copyright 2012 The Chromium Authors. All rights reserved. |
2 # 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 |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 import sys |
5 import logging | |
6 import unittest | |
7 | 5 |
8 from telemetry import decorators | 6 from telemetry import decorators |
9 from telemetry.core import browser_finder | 7 from telemetry.core import browser_finder |
10 from telemetry.core import browser_options | 8 from telemetry.core import browser_options |
11 from telemetry.core import command_line | 9 from telemetry.core import command_line |
12 from telemetry.core import discover | 10 from telemetry.core import util |
13 from telemetry.unittest_util import json_results | 11 from telemetry.unittest_util import options_for_unittests |
14 from telemetry.unittest_util import progress_reporter | 12 from telemetry.unittest_util import browser_test_case |
15 | 13 |
| 14 util.AddDirToPythonPath(util.GetChromiumSrcDir(), 'third_party', 'typ') |
16 | 15 |
17 class Config(object): | 16 import typ |
18 def __init__(self, top_level_dir, test_dirs, progress_reporters): | |
19 self._top_level_dir = top_level_dir | |
20 self._test_dirs = tuple(test_dirs) | |
21 self._progress_reporters = tuple(progress_reporters) | |
22 | |
23 @property | |
24 def top_level_dir(self): | |
25 return self._top_level_dir | |
26 | |
27 @property | |
28 def test_dirs(self): | |
29 return self._test_dirs | |
30 | |
31 @property | |
32 def progress_reporters(self): | |
33 return self._progress_reporters | |
34 | |
35 | |
36 def Discover(start_dir, top_level_dir=None, pattern='test*.py'): | |
37 loader = unittest.defaultTestLoader | |
38 loader.suiteClass = progress_reporter.TestSuite | |
39 | |
40 test_suites = [] | |
41 modules = discover.DiscoverModules(start_dir, top_level_dir, pattern) | |
42 for module in modules: | |
43 if hasattr(module, 'suite'): | |
44 suite = module.suite() | |
45 else: | |
46 suite = loader.loadTestsFromModule(module) | |
47 if suite.countTestCases(): | |
48 test_suites.append(suite) | |
49 return test_suites | |
50 | |
51 | |
52 def FilterSuite(suite, predicate): | |
53 new_suite = suite.__class__() | |
54 for test in suite: | |
55 if isinstance(test, unittest.TestSuite): | |
56 subsuite = FilterSuite(test, predicate) | |
57 if subsuite.countTestCases(): | |
58 new_suite.addTest(subsuite) | |
59 else: | |
60 assert isinstance(test, unittest.TestCase) | |
61 if predicate(test): | |
62 new_suite.addTest(test) | |
63 | |
64 return new_suite | |
65 | |
66 | |
67 def DiscoverTests(search_dirs, top_level_dir, possible_browser, | |
68 selected_tests=None, selected_tests_are_exact=False, | |
69 run_disabled_tests=False): | |
70 def IsTestSelected(test): | |
71 if selected_tests: | |
72 found = False | |
73 for name in selected_tests: | |
74 if selected_tests_are_exact: | |
75 if name == test.id(): | |
76 found = True | |
77 else: | |
78 if name in test.id(): | |
79 found = True | |
80 if not found: | |
81 return False | |
82 if run_disabled_tests: | |
83 return True | |
84 # pylint: disable=W0212 | |
85 if not hasattr(test, '_testMethodName'): | |
86 return True | |
87 method = getattr(test, test._testMethodName) | |
88 return decorators.IsEnabled(method, possible_browser) | |
89 | |
90 wrapper_suite = progress_reporter.TestSuite() | |
91 for search_dir in search_dirs: | |
92 wrapper_suite.addTests(Discover(search_dir, top_level_dir, '*_unittest.py')) | |
93 return FilterSuite(wrapper_suite, IsTestSelected) | |
94 | |
95 | |
96 def RestoreLoggingLevel(func): | |
97 def _LoggingRestoreWrapper(*args, **kwargs): | |
98 # Cache the current logging level, this needs to be done before calling | |
99 # parser.parse_args, which changes logging level based on verbosity | |
100 # setting. | |
101 logging_level = logging.getLogger().getEffectiveLevel() | |
102 try: | |
103 return func(*args, **kwargs) | |
104 finally: | |
105 # Restore logging level, which may be changed in parser.parse_args. | |
106 logging.getLogger().setLevel(logging_level) | |
107 | |
108 return _LoggingRestoreWrapper | |
109 | |
110 | |
111 config = None | |
112 | 17 |
113 | 18 |
114 class RunTestsCommand(command_line.OptparseCommand): | 19 class RunTestsCommand(command_line.OptparseCommand): |
115 """Run unit tests""" | 20 """Run unit tests""" |
116 | 21 |
117 usage = '[test_name ...] [<options>]' | 22 usage = '[test_name ...] [<options>]' |
118 | 23 |
| 24 def __init__(self): |
| 25 super(RunTestsCommand, self).__init__() |
| 26 self.stream = sys.stdout |
| 27 |
119 @classmethod | 28 @classmethod |
120 def CreateParser(cls): | 29 def CreateParser(cls): |
121 options = browser_options.BrowserFinderOptions() | 30 options = browser_options.BrowserFinderOptions() |
122 options.browser_type = 'any' | 31 options.browser_type = 'any' |
123 parser = options.CreateParser('%%prog %s' % cls.usage) | 32 parser = options.CreateParser('%%prog %s' % cls.usage) |
124 return parser | 33 return parser |
125 | 34 |
126 @classmethod | 35 @classmethod |
127 def AddCommandLineArgs(cls, parser): | 36 def AddCommandLineArgs(cls, parser): |
128 parser.add_option('--repeat-count', type='int', default=1, | 37 parser.add_option('--repeat-count', type='int', default=1, |
129 help='Repeats each a provided number of times.') | 38 help='Repeats each a provided number of times.') |
130 parser.add_option('-d', '--also-run-disabled-tests', | 39 parser.add_option('-d', '--also-run-disabled-tests', |
131 dest='run_disabled_tests', | 40 dest='run_disabled_tests', |
132 action='store_true', default=False, | 41 action='store_true', default=False, |
133 help='Ignore @Disabled and @Enabled restrictions.') | 42 help='Ignore @Disabled and @Enabled restrictions.') |
134 parser.add_option('--retry-limit', type='int', | |
135 help='Retry each failure up to N times' | |
136 ' to de-flake things.') | |
137 parser.add_option('--exact-test-filter', action='store_true', default=False, | 43 parser.add_option('--exact-test-filter', action='store_true', default=False, |
138 help='Treat test filter as exact matches (default is ' | 44 help='Treat test filter as exact matches (default is ' |
139 'substring matches).') | 45 'substring matches).') |
140 json_results.AddOptions(parser) | 46 |
| 47 typ.ArgumentParser.add_option_group(parser, |
| 48 "Options for running the tests", |
| 49 running=True, |
| 50 skip=['-d', '--path', '-v', |
| 51 '--verbose']) |
| 52 typ.ArgumentParser.add_option_group(parser, |
| 53 "Options for reporting the results", |
| 54 reporting=True) |
141 | 55 |
142 @classmethod | 56 @classmethod |
143 def ProcessCommandLineArgs(cls, parser, args): | 57 def ProcessCommandLineArgs(cls, parser, args): |
144 if args.verbosity == 0: | |
145 logging.getLogger().setLevel(logging.WARN) | |
146 | |
147 # We retry failures by default unless we're running a list of tests | 58 # We retry failures by default unless we're running a list of tests |
148 # explicitly. | 59 # explicitly. |
149 if args.retry_limit is None and not args.positional_args: | 60 if not args.retry_limit and not args.positional_args: |
150 args.retry_limit = 3 | 61 args.retry_limit = 3 |
151 | 62 |
152 try: | 63 try: |
153 possible_browser = browser_finder.FindBrowser(args) | 64 possible_browser = browser_finder.FindBrowser(args) |
154 except browser_finder.BrowserFinderException, ex: | 65 except browser_finder.BrowserFinderException, ex: |
155 parser.error(ex) | 66 parser.error(ex) |
156 | 67 |
157 if not possible_browser: | 68 if not possible_browser: |
158 parser.error('No browser found of type %s. Cannot run tests.\n' | 69 parser.error('No browser found of type %s. Cannot run tests.\n' |
159 'Re-run with --browser=list to see ' | 70 'Re-run with --browser=list to see ' |
160 'available browser types.' % args.browser_type) | 71 'available browser types.' % args.browser_type) |
161 | 72 |
162 json_results.ValidateArgs(parser, args) | 73 @classmethod |
| 74 def main(cls, args=None, stream=None): # pylint: disable=W0221 |
| 75 # We override the superclass so that we can hook in the 'stream' arg. |
| 76 parser = cls.CreateParser() |
| 77 cls.AddCommandLineArgs(parser) |
| 78 options, positional_args = parser.parse_args(args) |
| 79 options.positional_args = positional_args |
| 80 cls.ProcessCommandLineArgs(parser, options) |
| 81 |
| 82 obj = cls() |
| 83 if stream is not None: |
| 84 obj.stream = stream |
| 85 return obj.Run(options) |
163 | 86 |
164 def Run(self, args): | 87 def Run(self, args): |
165 possible_browser = browser_finder.FindBrowser(args) | 88 possible_browser = browser_finder.FindBrowser(args) |
166 | 89 |
167 test_suite, result = self.RunOneSuite(possible_browser, args) | 90 runner = typ.Runner() |
| 91 if self.stream: |
| 92 runner.host.stdout = self.stream |
168 | 93 |
169 results = [result] | 94 # Telemetry seems to overload the system if we run one test per core, |
| 95 # so we scale things back a fair amount. Many of the telemetry tests |
| 96 # are long-running, so there's a limit to how much parallelism we |
| 97 # can effectively use for now anyway. |
| 98 # |
| 99 # It should be possible to handle multiple devices if we adjust |
| 100 # the browser_finder code properly, but for now we only handle the one |
| 101 # on Android and ChromeOS. |
| 102 if possible_browser.platform.GetOSName() in ('android', 'chromeos'): |
| 103 runner.args.jobs = 1 |
| 104 else: |
| 105 runner.args.jobs = max(int(args.jobs) // 4, 1) |
170 | 106 |
171 failed_tests = json_results.FailedTestNames(test_suite, result) | 107 runner.args.metadata = args.metadata |
172 retry_limit = args.retry_limit | 108 runner.args.passthrough = args.passthrough |
| 109 runner.args.retry_limit = args.retry_limit |
| 110 runner.args.test_results_server = args.test_results_server |
| 111 runner.args.test_type = args.test_type |
| 112 runner.args.timing = args.timing |
| 113 runner.args.top_level_dir = args.top_level_dir |
| 114 runner.args.verbose = args.verbosity |
| 115 runner.args.write_full_results_to = args.write_full_results_to |
| 116 runner.args.write_trace_to = args.write_trace_to |
173 | 117 |
174 while retry_limit and failed_tests: | 118 runner.args.path.append(util.GetUnittestDataDir()) |
175 args.positional_args = failed_tests | |
176 args.exact_test_filter = True | |
177 | 119 |
178 _, result = self.RunOneSuite(possible_browser, args) | 120 runner.classifier = GetClassifier(args, possible_browser) |
179 results.append(result) | 121 runner.context = args |
| 122 runner.setup_fn = _SetUpProcess |
| 123 runner.teardown_fn = _TearDownProcess |
| 124 runner.win_multiprocessing = typ.WinMultiprocessing.importable |
| 125 try: |
| 126 ret, _, _ = runner.run() |
| 127 except KeyboardInterrupt: |
| 128 print >> sys.stderr, "interrupted, exiting" |
| 129 ret = 130 |
| 130 return ret |
180 | 131 |
181 failed_tests = json_results.FailedTestNames(test_suite, result) | |
182 retry_limit -= 1 | |
183 | 132 |
184 full_results = json_results.FullResults(args, test_suite, results) | 133 def GetClassifier(args, possible_browser): |
185 json_results.WriteFullResultsIfNecessary(args, full_results) | 134 def ClassifyTest(test_set, test): |
| 135 name = test.id() |
| 136 if args.positional_args: |
| 137 if _MatchesSelectedTest(name, args.positional_args, |
| 138 args.exact_test_filter): |
| 139 assert hasattr(test, '_testMethodName') |
| 140 method = getattr(test, test._testMethodName) # pylint: disable=W0212 |
| 141 if decorators.ShouldBeIsolated(method, possible_browser): |
| 142 test_set.isolated_tests.append(typ.TestInput(name)) |
| 143 else: |
| 144 test_set.parallel_tests.append(typ.TestInput(name)) |
| 145 else: |
| 146 assert hasattr(test, '_testMethodName') |
| 147 method = getattr(test, test._testMethodName) # pylint: disable=W0212 |
| 148 should_skip, reason = decorators.ShouldSkip(method, possible_browser) |
| 149 if should_skip and not args.run_disabled_tests: |
| 150 test_set.tests_to_skip.append(typ.TestInput(name, msg=reason)) |
| 151 elif decorators.ShouldBeIsolated(method, possible_browser): |
| 152 test_set.isolated_tests.append(typ.TestInput(name)) |
| 153 else: |
| 154 test_set.parallel_tests.append(typ.TestInput(name)) |
186 | 155 |
187 err_occurred, err_str = json_results.UploadFullResultsIfNecessary( | 156 return ClassifyTest |
188 args, full_results) | |
189 if err_occurred: | |
190 for line in err_str.splitlines(): | |
191 logging.error(line) | |
192 return 1 | |
193 | 157 |
194 return json_results.ExitCodeFromFullResults(full_results) | |
195 | 158 |
196 def RunOneSuite(self, possible_browser, args): | 159 def _MatchesSelectedTest(name, selected_tests, selected_tests_are_exact): |
197 test_suite = DiscoverTests(config.test_dirs, config.top_level_dir, | 160 if not selected_tests: |
198 possible_browser, args.positional_args, | 161 return False |
199 args.exact_test_filter, args.run_disabled_tests) | 162 if selected_tests_are_exact: |
200 runner = progress_reporter.TestRunner() | 163 return any(name in selected_tests) |
201 result = runner.run(test_suite, config.progress_reporters, | 164 else: |
202 args.repeat_count, args) | 165 return any(test in name for test in selected_tests) |
203 return test_suite, result | |
204 | 166 |
205 @classmethod | 167 |
206 @RestoreLoggingLevel | 168 def _SetUpProcess(child, context): # pylint: disable=W0613 |
207 def main(cls, args=None): | 169 args = context |
208 return super(RunTestsCommand, cls).main(args) | 170 options_for_unittests.Push(args) |
| 171 |
| 172 |
| 173 def _TearDownProcess(child, context): # pylint: disable=W0613 |
| 174 browser_test_case.teardown_browser() |
| 175 options_for_unittests.Pop() |
| 176 |
| 177 |
| 178 if __name__ == '__main__': |
| 179 ret_code = RunTestsCommand.main() |
| 180 sys.exit(ret_code) |
OLD | NEW |