OLD | NEW |
1 # Copyright 2014 The Chromium Authors. All rights reserved. | 1 # Copyright 2014 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 |
5 import logging | 5 import logging |
6 import optparse | 6 import optparse |
7 import os | 7 import os |
8 import random | 8 import random |
9 import sys | 9 import sys |
10 import time | 10 import time |
11 | 11 |
12 from telemetry import decorators | 12 from telemetry import decorators |
13 from telemetry.core import exceptions | 13 from telemetry.core import exceptions |
14 from telemetry.core import util | 14 from telemetry.core import util |
15 from telemetry.core import wpr_modes | 15 from telemetry.core import wpr_modes |
16 from telemetry.page import shared_page_state | 16 from telemetry.page import shared_page_state |
| 17 from telemetry.page import page_set as page_set_module |
17 from telemetry.page import page_test | 18 from telemetry.page import page_test |
18 from telemetry.page.actions import page_action | 19 from telemetry.page.actions import page_action |
19 from telemetry.results import results_options | 20 from telemetry.results import results_options |
20 from telemetry.user_story import user_story_filter | 21 from telemetry.user_story import user_story_filter |
21 from telemetry.util import cloud_storage | 22 from telemetry.util import cloud_storage |
22 from telemetry.util import exception_formatter | 23 from telemetry.util import exception_formatter |
23 from telemetry.value import failure | 24 from telemetry.value import failure |
24 from telemetry.value import skip | 25 from telemetry.value import skip |
25 | 26 |
26 | 27 |
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
68 # Page set options | 69 # Page set options |
69 if args.pageset_shuffle_order_file and not args.pageset_shuffle: | 70 if args.pageset_shuffle_order_file and not args.pageset_shuffle: |
70 parser.error('--pageset-shuffle-order-file requires --pageset-shuffle.') | 71 parser.error('--pageset-shuffle-order-file requires --pageset-shuffle.') |
71 | 72 |
72 if args.page_repeat < 1: | 73 if args.page_repeat < 1: |
73 parser.error('--page-repeat must be a positive integer.') | 74 parser.error('--page-repeat must be a positive integer.') |
74 if args.pageset_repeat < 1: | 75 if args.pageset_repeat < 1: |
75 parser.error('--pageset-repeat must be a positive integer.') | 76 parser.error('--pageset-repeat must be a positive integer.') |
76 | 77 |
77 | 78 |
78 def _RunPageAndHandleExceptionIfNeeded(test, page_set, expectations, | 79 def _RunUserStoryAndProcessErrorIfNeeded( |
79 page, results, state): | 80 test, expectations, user_story, results, state): |
80 expectation = None | 81 expectation = None |
81 def ProcessError(): | 82 def ProcessError(): |
82 if expectation == 'fail': | 83 if expectation == 'fail': |
83 msg = 'Expected exception while running %s' % page.url | 84 msg = 'Expected exception while running %s' % user_story.display_name |
84 exception_formatter.PrintFormattedException(msg=msg) | 85 exception_formatter.PrintFormattedException(msg=msg) |
85 else: | 86 else: |
86 msg = 'Exception while running %s' % page.url | 87 msg = 'Exception while running %s' % user_story.display_name |
87 results.AddValue(failure.FailureValue(page, sys.exc_info())) | 88 results.AddValue(failure.FailureValue(user_story, sys.exc_info())) |
88 | 89 |
89 try: | 90 try: |
90 state.WillRunPage(page, page_set) | 91 state.WillRunUserStory(user_story) |
91 expectation, skip_value = state.GetPageExpectationAndSkipValue(expectations) | 92 expectation, skip_value = state.GetTestExpectationAndSkipValue(expectations) |
92 if expectation == 'skip': | 93 if expectation == 'skip': |
93 assert skip_value | 94 assert skip_value |
94 results.AddValue(skip_value) | 95 results.AddValue(skip_value) |
95 return | 96 return |
96 state.RunPage(results) | 97 state.RunUserStory(results) |
97 except page_test.TestNotSupportedOnPlatformFailure: | 98 except page_test.TestNotSupportedOnPlatformFailure: |
98 raise | 99 raise |
99 except (page_test.Failure, util.TimeoutException, exceptions.LoginException, | 100 except (page_test.Failure, util.TimeoutException, exceptions.LoginException, |
100 exceptions.ProfilingException): | 101 exceptions.ProfilingException): |
101 ProcessError() | 102 ProcessError() |
102 except exceptions.AppCrashException: | 103 except exceptions.AppCrashException: |
103 ProcessError() | 104 ProcessError() |
104 state.TearDown(results) | 105 state.TearDownState(results) |
105 if test.is_multi_tab_test: | 106 if test.is_multi_tab_test: |
106 logging.error('Aborting multi-tab test after browser or tab crashed at ' | 107 logging.error('Aborting multi-tab test after browser or tab crashed at ' |
107 'page %s' % page.url) | 108 'user story %s' % user_story.display_name) |
108 test.RequestExit() | 109 test.RequestExit() |
109 return | 110 return |
110 except page_action.PageActionNotSupported as e: | 111 except page_action.PageActionNotSupported as e: |
111 results.AddValue(skip.SkipValue(page, 'Unsupported page action: %s' % e)) | 112 results.AddValue( |
| 113 skip.SkipValue(user_story, 'Unsupported page action: %s' % e)) |
112 except Exception: | 114 except Exception: |
113 exception_formatter.PrintFormattedException( | 115 exception_formatter.PrintFormattedException( |
114 msg='Unhandled exception while running %s' % page.url) | 116 msg='Unhandled exception while running %s' % user_story.display_name) |
115 results.AddValue(failure.FailureValue(page, sys.exc_info())) | 117 results.AddValue(failure.FailureValue(user_story, sys.exc_info())) |
116 else: | 118 else: |
117 if expectation == 'fail': | 119 if expectation == 'fail': |
118 logging.warning('%s was expected to fail, but passed.\n', page.url) | 120 logging.warning( |
| 121 '%s was expected to fail, but passed.\n', user_story.display_name) |
119 finally: | 122 finally: |
120 state.DidRunPage(results) | 123 state.DidRunUserStory(results) |
121 | 124 |
122 | 125 |
123 @decorators.Cache | 126 @decorators.Cache |
124 def _UpdatePageSetArchivesIfChanged(page_set): | 127 def _UpdateUserStoryArchivesIfChanged(page_set): |
125 # Scan every serving directory for .sha1 files | 128 # Scan every serving directory for .sha1 files |
126 # and download them from Cloud Storage. Assume all data is public. | 129 # and download them from Cloud Storage. Assume all data is public. |
127 all_serving_dirs = page_set.serving_dirs.copy() | 130 all_serving_dirs = page_set.serving_dirs.copy() |
128 # Add individual page dirs to all serving dirs. | 131 # Add individual page dirs to all serving dirs. |
129 for page in page_set: | 132 for page in page_set: |
130 if page.is_file: | 133 if page.is_file: |
131 all_serving_dirs.add(page.serving_dir) | 134 all_serving_dirs.add(page.serving_dir) |
132 # Scan all serving dirs. | 135 # Scan all serving dirs. |
133 for serving_dir in all_serving_dirs: | 136 for serving_dir in all_serving_dirs: |
134 if os.path.splitdrive(serving_dir)[1] == '/': | 137 if os.path.splitdrive(serving_dir)[1] == '/': |
135 raise ValueError('Trying to serve root directory from HTTP server.') | 138 raise ValueError('Trying to serve root directory from HTTP server.') |
136 for dirpath, _, filenames in os.walk(serving_dir): | 139 for dirpath, _, filenames in os.walk(serving_dir): |
137 for filename in filenames: | 140 for filename in filenames: |
138 path, extension = os.path.splitext( | 141 path, extension = os.path.splitext( |
139 os.path.join(dirpath, filename)) | 142 os.path.join(dirpath, filename)) |
140 if extension != '.sha1': | 143 if extension != '.sha1': |
141 continue | 144 continue |
142 cloud_storage.GetIfChanged(path, page_set.bucket) | 145 cloud_storage.GetIfChanged(path, page_set.bucket) |
143 | 146 |
144 | 147 |
145 def Run(test, page_set, expectations, finder_options, results): | 148 class UserStoryGroup(object): |
| 149 def __init__(self, shared_user_story_state_class): |
| 150 self._shared_user_story_state_class = shared_user_story_state_class |
| 151 self._user_stories = [] |
| 152 |
| 153 @property |
| 154 def shared_user_story_state_class(self): |
| 155 return self._shared_user_story_state_class |
| 156 |
| 157 @property |
| 158 def user_stories(self): |
| 159 return self._user_stories |
| 160 |
| 161 def AddUserStory(self, user_story): |
| 162 assert (user_story.shared_user_story_state_class is |
| 163 self._shared_user_story_state_class) |
| 164 self._user_stories.append(user_story) |
| 165 |
| 166 |
| 167 def GetUserStoryGroupsWithSameSharedUserStoryClass(user_story_set): |
| 168 """ Returns a list of user story groups which each contains user stories with |
| 169 the same shared_user_story_state_class. |
| 170 |
| 171 Example: |
| 172 Assume A1, A2, A3 are user stories with same shared user story class, and |
| 173 similar for B1, B2. |
| 174 If their orders in user story set is A1 A2 B1 B2 A3, then the grouping will |
| 175 be [A1 A2] [B1 B2] [A3]. |
| 176 |
| 177 It's purposefully done this way to make sure that order of user |
| 178 stories are the same of that defined in user_story_set. It's recommended that |
| 179 user stories with the same states should be arranged next to each others in |
| 180 user story sets to reduce the overhead of setting up & tearing down the |
| 181 shared user story state. |
| 182 """ |
| 183 user_story_groups = [] |
| 184 user_story_groups.append( |
| 185 UserStoryGroup(user_story_set[0].shared_user_story_state_class)) |
| 186 for user_story in user_story_set: |
| 187 if (user_story.shared_user_story_state_class is not |
| 188 user_story_groups[-1].shared_user_story_state_class): |
| 189 user_story_groups.append( |
| 190 UserStoryGroup(user_story.shared_user_story_state_class)) |
| 191 user_story_groups[-1].AddUserStory(user_story) |
| 192 return user_story_groups |
| 193 |
| 194 |
| 195 def Run(test, user_story_set, expectations, finder_options, results): |
146 """Runs a given test against a given page_set with the given options.""" | 196 """Runs a given test against a given page_set with the given options.""" |
147 test.ValidatePageSet(page_set) | 197 test.ValidatePageSet(user_story_set) |
148 | 198 |
149 # Reorder page set based on options. | 199 # Reorder page set based on options. |
150 pages = _ShuffleAndFilterPageSet(page_set, finder_options) | 200 user_stories = _ShuffleAndFilterUserStorySet(user_story_set, finder_options) |
151 | 201 |
152 if not finder_options.use_live_sites: | 202 if (not finder_options.use_live_sites and |
153 if finder_options.browser_options.wpr_mode != wpr_modes.WPR_RECORD: | 203 finder_options.browser_options.wpr_mode != wpr_modes.WPR_RECORD and |
154 _UpdatePageSetArchivesIfChanged(page_set) | 204 # TODO(nednguyen): also handle these logic for user_story_set in next |
155 pages = _CheckArchives(page_set, pages, results) | 205 # patch. |
| 206 isinstance(user_story_set, page_set_module.PageSet)): |
| 207 _UpdateUserStoryArchivesIfChanged(user_story_set) |
| 208 user_stories = _CheckArchives(user_story_set, user_stories, results) |
156 | 209 |
157 for page in list(pages): | 210 for user_story in list(user_stories): |
158 if not test.CanRunForPage(page): | 211 if not test.CanRunForPage(user_story): |
159 results.WillRunPage(page) | 212 results.WillRunPage(user_story) |
160 logging.debug('Skipping test: it cannot run for %s', page.url) | 213 logging.debug('Skipping test: it cannot run for %s', |
161 results.AddValue(skip.SkipValue(page, 'Test cannot run')) | 214 user_story.display_name) |
162 results.DidRunPage(page) | 215 results.AddValue(skip.SkipValue(user_story, 'Test cannot run')) |
163 pages.remove(page) | 216 results.DidRunPage(user_story) |
| 217 user_stories.remove(user_story) |
164 | 218 |
165 if not pages: | 219 if not user_stories: |
166 return | 220 return |
167 | 221 |
168 state = shared_page_state.SharedPageState(test, finder_options, page_set) | 222 user_story_with_discarded_first_results = set() |
169 pages_with_discarded_first_result = set() | |
170 max_failures = finder_options.max_failures # command-line gets priority | 223 max_failures = finder_options.max_failures # command-line gets priority |
171 if max_failures is None: | 224 if max_failures is None: |
172 max_failures = test.max_failures # may be None | 225 max_failures = test.max_failures # may be None |
| 226 user_story_groups = GetUserStoryGroupsWithSameSharedUserStoryClass( |
| 227 user_stories) |
173 | 228 |
174 try: | 229 test.WillRunTest(finder_options) |
175 test.WillRunTest(finder_options) | 230 state = None |
176 for _ in xrange(finder_options.pageset_repeat): | 231 for _ in xrange(finder_options.pageset_repeat): |
177 for page in pages: | 232 for group in user_story_groups: |
178 if test.IsExiting(): | 233 try: |
179 break | 234 state = group.shared_user_story_state_class( |
180 for _ in xrange(finder_options.page_repeat): | 235 test, finder_options, user_story_set) |
181 results.WillRunPage(page) | 236 for user_story in group.user_stories: |
182 try: | 237 if test.IsExiting(): |
183 _WaitForThermalThrottlingIfNeeded(state.platform) | 238 break |
184 _RunPageAndHandleExceptionIfNeeded( | 239 for _ in xrange(finder_options.page_repeat): |
185 test, page_set, expectations, page, results, state) | 240 results.WillRunPage(user_story) |
186 except Exception: | 241 try: |
187 # Tear down & restart the state for unhandled exceptions thrown by | 242 _WaitForThermalThrottlingIfNeeded(state.platform) |
188 # _RunPageAndHandleExceptionIfNeeded. | 243 _RunUserStoryAndProcessErrorIfNeeded( |
189 results.AddValue(failure.FailureValue(page, sys.exc_info())) | 244 test, expectations, user_story, results, state) |
190 state.TearDown(results) | 245 except Exception: |
191 state = shared_page_state.SharedPageState( | 246 # Tear down & restart the state for unhandled exceptions thrown by |
192 test, finder_options, page_set) | 247 # _RunUserStoryAndProcessErrorIfNeeded. |
193 finally: | 248 results.AddValue(failure.FailureValue(user_story, sys.exc_info())) |
194 _CheckThermalThrottling(state.platform) | 249 state.TearDownState(results) |
195 discard_run = (test.discard_first_result and | 250 state = group.shared_user_story_state_class( |
196 page not in pages_with_discarded_first_result) | 251 test, finder_options, user_story_set) |
197 if discard_run: | 252 finally: |
198 pages_with_discarded_first_result.add(page) | 253 _CheckThermalThrottling(state.platform) |
199 results.DidRunPage(page, discard_run=discard_run) | 254 discard_run = (test.discard_first_result and |
200 if max_failures is not None and len(results.failures) > max_failures: | 255 user_story not in |
201 logging.error('Too many failures. Aborting.') | 256 user_story_with_discarded_first_results) |
202 test.RequestExit() | 257 if discard_run: |
203 finally: | 258 user_story_with_discarded_first_results.add(user_story) |
204 state.TearDown(results) | 259 results.DidRunPage(user_story, discard_run=discard_run) |
| 260 if max_failures is not None and len(results.failures) > max_failures: |
| 261 logging.error('Too many failures. Aborting.') |
| 262 test.RequestExit() |
| 263 finally: |
| 264 if state: |
| 265 state.TearDownState(results) |
205 | 266 |
206 | 267 def _ShuffleAndFilterUserStorySet(user_story_set, finder_options): |
207 def _ShuffleAndFilterPageSet(page_set, finder_options): | |
208 if finder_options.pageset_shuffle_order_file: | 268 if finder_options.pageset_shuffle_order_file: |
209 return page_set.ReorderPageSet(finder_options.pageset_shuffle_order_file) | 269 if isinstance(user_story_set, page_set_module.PageSet): |
210 pages = [page for page in page_set.pages[:] | 270 return page_set_module.ReorderPageSet( |
211 if user_story_filter.UserStoryFilter.IsSelected(page)] | 271 finder_options.pageset_shuffle_order_file) |
| 272 else: |
| 273 raise Exception( |
| 274 'pageset-shuffle-order-file flag can only be used with page set') |
| 275 user_stories = [u for u in user_story_set[:] |
| 276 if user_story_filter.UserStoryFilter.IsSelected(u)] |
212 if finder_options.pageset_shuffle: | 277 if finder_options.pageset_shuffle: |
213 random.shuffle(pages) | 278 random.shuffle(user_stories) |
214 return pages | 279 return user_stories |
215 | 280 |
216 | 281 |
217 def _CheckArchives(page_set, pages, results): | 282 def _CheckArchives(page_set, pages, results): |
218 """Returns a subset of pages that are local or have WPR archives. | 283 """Returns a subset of pages that are local or have WPR archives. |
219 | 284 |
220 Logs warnings if any are missing. | 285 Logs warnings if any are missing. |
221 """ | 286 """ |
222 # Warn of any problems with the entire page set. | 287 # Warn of any problems with the entire page set. |
223 if any(not p.is_local for p in pages): | 288 if any(not p.is_local for p in pages): |
224 if not page_set.archive_data_file: | 289 if not page_set.archive_data_file: |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
276 logging.warning('Device is thermally throttled before running ' | 341 logging.warning('Device is thermally throttled before running ' |
277 'performance tests, results will vary.') | 342 'performance tests, results will vary.') |
278 | 343 |
279 | 344 |
280 def _CheckThermalThrottling(platform): | 345 def _CheckThermalThrottling(platform): |
281 if not platform.CanMonitorThermalThrottling(): | 346 if not platform.CanMonitorThermalThrottling(): |
282 return | 347 return |
283 if platform.HasBeenThermallyThrottled(): | 348 if platform.HasBeenThermallyThrottled(): |
284 logging.warning('Device has been thermally throttled during ' | 349 logging.warning('Device has been thermally throttled during ' |
285 'performance tests, results will vary.') | 350 'performance tests, results will vary.') |
OLD | NEW |